Repository: twbs/bootstrap Branch: main Commit: 060ff24924a2 Files: 609 Total size: 4.5 MB Directory structure: gitextract_muvc3lnh/ ├── .babelrc.js ├── .browserslistrc ├── .bundlewatch.config.json ├── .cspell.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── INCIDENT_RESPONSE.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SUPPORT.md │ ├── codeql/ │ │ └── codeql-config.yml │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── browserstack.yml │ ├── bundlewatch.yml │ ├── calibreapp-image-actions.yml │ ├── codeql.yml │ ├── cspell.yml │ ├── css.yml │ ├── docs.yml │ ├── issue-close-require.yml │ ├── issue-labeled.yml │ ├── js.yml │ ├── lint.yml │ ├── node-sass.yml │ ├── publish-nuget.yml │ ├── release-notes.yml │ └── scorecard.yml ├── .gitignore ├── .prettierignore ├── .stylelintignore ├── .stylelintrc.json ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build/ │ ├── banner.mjs │ ├── build-plugins.mjs │ ├── change-version.mjs │ ├── docs-prep.sh │ ├── generate-sri.mjs │ ├── postcss.config.mjs │ ├── rollup.config.mjs │ ├── vnu-jar.mjs │ └── zip-examples.mjs ├── composer.json ├── config.yml ├── dist/ │ ├── css/ │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.rtl.css │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.rtl.css │ │ ├── bootstrap-utilities.css │ │ ├── bootstrap-utilities.rtl.css │ │ ├── bootstrap.css │ │ └── bootstrap.rtl.css │ └── js/ │ ├── bootstrap.bundle.js │ ├── bootstrap.esm.js │ └── bootstrap.js ├── js/ │ ├── dist/ │ │ ├── alert.js │ │ ├── base-component.js │ │ ├── button.js │ │ ├── carousel.js │ │ ├── collapse.js │ │ ├── dom/ │ │ │ ├── data.js │ │ │ ├── event-handler.js │ │ │ ├── manipulator.js │ │ │ └── selector-engine.js │ │ ├── dropdown.js │ │ ├── modal.js │ │ ├── offcanvas.js │ │ ├── popover.js │ │ ├── scrollspy.js │ │ ├── tab.js │ │ ├── toast.js │ │ ├── tooltip.js │ │ └── util/ │ │ ├── backdrop.js │ │ ├── component-functions.js │ │ ├── config.js │ │ ├── focustrap.js │ │ ├── index.js │ │ ├── sanitizer.js │ │ ├── scrollbar.js │ │ ├── swipe.js │ │ └── template-factory.js │ ├── index.esm.js │ ├── index.umd.js │ ├── src/ │ │ ├── alert.js │ │ ├── base-component.js │ │ ├── button.js │ │ ├── carousel.js │ │ ├── collapse.js │ │ ├── dom/ │ │ │ ├── data.js │ │ │ ├── event-handler.js │ │ │ ├── manipulator.js │ │ │ └── selector-engine.js │ │ ├── dropdown.js │ │ ├── modal.js │ │ ├── offcanvas.js │ │ ├── popover.js │ │ ├── scrollspy.js │ │ ├── tab.js │ │ ├── toast.js │ │ ├── tooltip.js │ │ └── util/ │ │ ├── backdrop.js │ │ ├── component-functions.js │ │ ├── config.js │ │ ├── focustrap.js │ │ ├── index.js │ │ ├── sanitizer.js │ │ ├── scrollbar.js │ │ ├── swipe.js │ │ └── template-factory.js │ └── tests/ │ ├── README.md │ ├── browsers.js │ ├── helpers/ │ │ └── fixture.js │ ├── integration/ │ │ ├── bundle-modularity.js │ │ ├── bundle.js │ │ ├── index.html │ │ ├── rollup.bundle-modularity.js │ │ └── rollup.bundle.js │ ├── karma.conf.js │ ├── unit/ │ │ ├── alert.spec.js │ │ ├── base-component.spec.js │ │ ├── button.spec.js │ │ ├── carousel.spec.js │ │ ├── collapse.spec.js │ │ ├── dom/ │ │ │ ├── data.spec.js │ │ │ ├── event-handler.spec.js │ │ │ ├── manipulator.spec.js │ │ │ └── selector-engine.spec.js │ │ ├── dropdown.spec.js │ │ ├── jquery.spec.js │ │ ├── modal.spec.js │ │ ├── offcanvas.spec.js │ │ ├── popover.spec.js │ │ ├── scrollspy.spec.js │ │ ├── tab.spec.js │ │ ├── toast.spec.js │ │ ├── tooltip.spec.js │ │ └── util/ │ │ ├── backdrop.spec.js │ │ ├── component-functions.spec.js │ │ ├── config.spec.js │ │ ├── focustrap.spec.js │ │ ├── index.spec.js │ │ ├── sanitizer.spec.js │ │ ├── scrollbar.spec.js │ │ ├── swipe.spec.js │ │ └── template-factory.spec.js │ └── visual/ │ ├── alert.html │ ├── button.html │ ├── carousel.html │ ├── collapse.html │ ├── dropdown.html │ ├── floating-label.html │ ├── input.html │ ├── modal.html │ ├── popover.html │ ├── scrollspy.html │ ├── tab.html │ ├── toast.html │ └── tooltip.html ├── nuget/ │ ├── bootstrap.nuspec │ └── bootstrap.sass.nuspec ├── package.js ├── package.json ├── scss/ │ ├── _accordion.scss │ ├── _alert.scss │ ├── _badge.scss │ ├── _breadcrumb.scss │ ├── _button-group.scss │ ├── _buttons.scss │ ├── _card.scss │ ├── _carousel.scss │ ├── _close.scss │ ├── _containers.scss │ ├── _dropdown.scss │ ├── _forms.scss │ ├── _functions.scss │ ├── _grid.scss │ ├── _helpers.scss │ ├── _images.scss │ ├── _list-group.scss │ ├── _maps.scss │ ├── _mixins.scss │ ├── _modal.scss │ ├── _nav.scss │ ├── _navbar.scss │ ├── _offcanvas.scss │ ├── _pagination.scss │ ├── _placeholders.scss │ ├── _popover.scss │ ├── _progress.scss │ ├── _reboot.scss │ ├── _root.scss │ ├── _spinners.scss │ ├── _tables.scss │ ├── _toasts.scss │ ├── _tooltip.scss │ ├── _transitions.scss │ ├── _type.scss │ ├── _utilities.scss │ ├── _variables-dark.scss │ ├── _variables.scss │ ├── bootstrap-grid.scss │ ├── bootstrap-reboot.scss │ ├── bootstrap-utilities.scss │ ├── bootstrap.scss │ ├── forms/ │ │ ├── _floating-labels.scss │ │ ├── _form-check.scss │ │ ├── _form-control.scss │ │ ├── _form-range.scss │ │ ├── _form-select.scss │ │ ├── _form-text.scss │ │ ├── _input-group.scss │ │ ├── _labels.scss │ │ └── _validation.scss │ ├── helpers/ │ │ ├── _clearfix.scss │ │ ├── _color-bg.scss │ │ ├── _colored-links.scss │ │ ├── _focus-ring.scss │ │ ├── _icon-link.scss │ │ ├── _position.scss │ │ ├── _ratio.scss │ │ ├── _stacks.scss │ │ ├── _stretched-link.scss │ │ ├── _text-truncation.scss │ │ ├── _visually-hidden.scss │ │ └── _vr.scss │ ├── mixins/ │ │ ├── _alert.scss │ │ ├── _backdrop.scss │ │ ├── _banner.scss │ │ ├── _border-radius.scss │ │ ├── _box-shadow.scss │ │ ├── _breakpoints.scss │ │ ├── _buttons.scss │ │ ├── _caret.scss │ │ ├── _clearfix.scss │ │ ├── _color-mode.scss │ │ ├── _color-scheme.scss │ │ ├── _container.scss │ │ ├── _deprecate.scss │ │ ├── _forms.scss │ │ ├── _gradients.scss │ │ ├── _grid.scss │ │ ├── _image.scss │ │ ├── _list-group.scss │ │ ├── _lists.scss │ │ ├── _pagination.scss │ │ ├── _reset-text.scss │ │ ├── _resize.scss │ │ ├── _table-variants.scss │ │ ├── _text-truncate.scss │ │ ├── _transition.scss │ │ ├── _utilities.scss │ │ └── _visually-hidden.scss │ ├── tests/ │ │ ├── jasmine.js │ │ ├── mixins/ │ │ │ ├── _auto-import-of-variables-dark.test.scss │ │ │ ├── _box-shadow.test.scss │ │ │ ├── _color-contrast.test.scss │ │ │ ├── _color-modes.test.scss │ │ │ ├── _media-query-color-mode-full.test.scss │ │ │ └── _utilities.test.scss │ │ ├── sass-true/ │ │ │ ├── register.js │ │ │ └── runner.js │ │ └── utilities/ │ │ └── _api.test.scss │ ├── utilities/ │ │ └── _api.scss │ └── vendor/ │ └── _rfs.scss └── site/ ├── .prettierrc.json ├── astro.config.ts ├── data/ │ ├── breakpoints.yml │ ├── colors.yml │ ├── core-team.yml │ ├── docs-versions.yml │ ├── examples.yml │ ├── grays.yml │ ├── icons.yml │ ├── plugins.yml │ ├── sidebar.yml │ ├── theme-colors.yml │ └── translations.yml ├── postcss.config.cjs ├── src/ │ ├── assets/ │ │ ├── application.js │ │ ├── examples/ │ │ │ ├── album/ │ │ │ │ └── index.astro │ │ │ ├── album-rtl/ │ │ │ │ └── index.astro │ │ │ ├── badges/ │ │ │ │ ├── badges.css │ │ │ │ └── index.astro │ │ │ ├── blog/ │ │ │ │ ├── blog.css │ │ │ │ ├── blog.rtl.css │ │ │ │ └── index.astro │ │ │ ├── blog-rtl/ │ │ │ │ └── index.astro │ │ │ ├── breadcrumbs/ │ │ │ │ ├── breadcrumbs.css │ │ │ │ └── index.astro │ │ │ ├── buttons/ │ │ │ │ └── index.astro │ │ │ ├── carousel/ │ │ │ │ ├── carousel.css │ │ │ │ ├── carousel.rtl.css │ │ │ │ └── index.astro │ │ │ ├── carousel-rtl/ │ │ │ │ └── index.astro │ │ │ ├── cheatsheet/ │ │ │ │ ├── cheatsheet.css │ │ │ │ ├── cheatsheet.js │ │ │ │ ├── cheatsheet.rtl.css │ │ │ │ └── index.astro │ │ │ ├── cheatsheet-rtl/ │ │ │ │ └── index.astro │ │ │ ├── checkout/ │ │ │ │ ├── checkout.css │ │ │ │ ├── checkout.js │ │ │ │ └── index.astro │ │ │ ├── checkout-rtl/ │ │ │ │ └── index.astro │ │ │ ├── cover/ │ │ │ │ ├── cover.css │ │ │ │ └── index.astro │ │ │ ├── dashboard/ │ │ │ │ ├── dashboard.css │ │ │ │ ├── dashboard.js │ │ │ │ ├── dashboard.rtl.css │ │ │ │ └── index.astro │ │ │ ├── dashboard-rtl/ │ │ │ │ ├── dashboard.js │ │ │ │ └── index.astro │ │ │ ├── dropdowns/ │ │ │ │ ├── dropdowns.css │ │ │ │ └── index.astro │ │ │ ├── features/ │ │ │ │ ├── features.css │ │ │ │ └── index.astro │ │ │ ├── footers/ │ │ │ │ └── index.astro │ │ │ ├── grid/ │ │ │ │ ├── grid.css │ │ │ │ └── index.astro │ │ │ ├── headers/ │ │ │ │ ├── headers.css │ │ │ │ └── index.astro │ │ │ ├── heroes/ │ │ │ │ ├── heroes.css │ │ │ │ └── index.astro │ │ │ ├── jumbotron/ │ │ │ │ └── index.astro │ │ │ ├── jumbotrons/ │ │ │ │ ├── index.astro │ │ │ │ └── jumbotrons.css │ │ │ ├── list-groups/ │ │ │ │ ├── index.astro │ │ │ │ └── list-groups.css │ │ │ ├── masonry/ │ │ │ │ └── index.astro │ │ │ ├── modals/ │ │ │ │ ├── index.astro │ │ │ │ └── modals.css │ │ │ ├── navbar-bottom/ │ │ │ │ └── index.astro │ │ │ ├── navbar-fixed/ │ │ │ │ ├── index.astro │ │ │ │ └── navbar-fixed.css │ │ │ ├── navbar-static/ │ │ │ │ ├── index.astro │ │ │ │ └── navbar-static.css │ │ │ ├── navbars/ │ │ │ │ ├── index.astro │ │ │ │ └── navbars.css │ │ │ ├── navbars-offcanvas/ │ │ │ │ ├── index.astro │ │ │ │ └── navbars-offcanvas.css │ │ │ ├── offcanvas-navbar/ │ │ │ │ ├── index.astro │ │ │ │ ├── offcanvas-navbar.css │ │ │ │ └── offcanvas-navbar.js │ │ │ ├── pricing/ │ │ │ │ ├── index.astro │ │ │ │ └── pricing.css │ │ │ ├── product/ │ │ │ │ ├── index.astro │ │ │ │ └── product.css │ │ │ ├── sidebars/ │ │ │ │ ├── index.astro │ │ │ │ ├── sidebars.css │ │ │ │ └── sidebars.js │ │ │ ├── sign-in/ │ │ │ │ ├── index.astro │ │ │ │ └── sign-in.css │ │ │ ├── starter-template/ │ │ │ │ └── index.astro │ │ │ ├── sticky-footer/ │ │ │ │ ├── index.astro │ │ │ │ └── sticky-footer.css │ │ │ └── sticky-footer-navbar/ │ │ │ ├── index.astro │ │ │ └── sticky-footer-navbar.css │ │ ├── partials/ │ │ │ ├── sidebar.js │ │ │ └── snippets.js │ │ ├── search.js │ │ ├── snippets.js │ │ └── stackblitz.js │ ├── components/ │ │ ├── Ads.astro │ │ ├── DocsScripts.astro │ │ ├── DocsSidebar.astro │ │ ├── Scripts.astro │ │ ├── TableOfContents.astro │ │ ├── footer/ │ │ │ └── Footer.astro │ │ ├── head/ │ │ │ ├── Analytics.astro │ │ │ ├── Favicons.astro │ │ │ ├── Head.astro │ │ │ ├── Scss.astro │ │ │ ├── ScssProd.astro │ │ │ ├── Social.astro │ │ │ └── Stylesheet.astro │ │ ├── header/ │ │ │ ├── Header.astro │ │ │ ├── LinkItem.astro │ │ │ ├── Navigation.astro │ │ │ ├── Skippy.astro │ │ │ └── Versions.astro │ │ ├── home/ │ │ │ ├── CSSVariables.astro │ │ │ ├── ComponentUtilities.astro │ │ │ ├── Customize.astro │ │ │ ├── GetStarted.astro │ │ │ ├── Icons.astro │ │ │ ├── MastHead.astro │ │ │ └── Plugins.astro │ │ ├── icons/ │ │ │ ├── BootstrapWhiteFillIcon.astro │ │ │ ├── CircleSquareIcon.astro │ │ │ ├── DropletFillIcon.astro │ │ │ ├── GitHubIcon.astro │ │ │ ├── HamburgerIcon.astro │ │ │ ├── OpenCollectiveIcon.astro │ │ │ ├── Symbols.astro │ │ │ └── XIcon.astro │ │ └── shortcodes/ │ │ ├── AddedIn.astro │ │ ├── BsTable.astro │ │ ├── Callout.astro │ │ ├── CalloutDeprecatedDarkVariants.astro │ │ ├── Code.astro │ │ ├── DeprecatedIn.astro │ │ ├── Example.astro │ │ ├── GuideFooter.mdx │ │ ├── JsDataAttributes.mdx │ │ ├── JsDismiss.astro │ │ ├── JsDocs.astro │ │ ├── Placeholder.astro │ │ ├── ScssDocs.astro │ │ ├── Table.astro │ │ └── TableContent.md │ ├── content/ │ │ ├── callouts/ │ │ │ ├── danger-async-methods.md │ │ │ ├── info-mediaqueries-breakpoints.md │ │ │ ├── info-npm-starter.md │ │ │ ├── info-prefersreducedmotion.md │ │ │ ├── info-sanitizer.md │ │ │ ├── warning-color-assistive-technologies.md │ │ │ ├── warning-data-bs-title-vs-title.md │ │ │ └── warning-input-support.md │ │ ├── config.ts │ │ └── docs/ │ │ ├── about/ │ │ │ ├── brand.mdx │ │ │ ├── license.mdx │ │ │ ├── overview.mdx │ │ │ ├── team.mdx │ │ │ └── translations.mdx │ │ ├── components/ │ │ │ ├── accordion.mdx │ │ │ ├── alerts.mdx │ │ │ ├── badge.mdx │ │ │ ├── breadcrumb.mdx │ │ │ ├── button-group.mdx │ │ │ ├── buttons.mdx │ │ │ ├── card.mdx │ │ │ ├── carousel.mdx │ │ │ ├── close-button.mdx │ │ │ ├── collapse.mdx │ │ │ ├── dropdowns.mdx │ │ │ ├── list-group.mdx │ │ │ ├── modal.mdx │ │ │ ├── navbar.mdx │ │ │ ├── navs-tabs.mdx │ │ │ ├── offcanvas.mdx │ │ │ ├── pagination.mdx │ │ │ ├── placeholders.mdx │ │ │ ├── popovers.mdx │ │ │ ├── progress.mdx │ │ │ ├── scrollspy.mdx │ │ │ ├── spinners.mdx │ │ │ ├── toasts.mdx │ │ │ └── tooltips.mdx │ │ ├── content/ │ │ │ ├── figures.mdx │ │ │ ├── images.mdx │ │ │ ├── reboot.mdx │ │ │ ├── tables.mdx │ │ │ └── typography.mdx │ │ ├── customize/ │ │ │ ├── color-modes.mdx │ │ │ ├── color.mdx │ │ │ ├── components.mdx │ │ │ ├── css-variables.mdx │ │ │ ├── optimize.mdx │ │ │ ├── options.mdx │ │ │ ├── overview.mdx │ │ │ └── sass.mdx │ │ ├── docsref.mdx │ │ ├── extend/ │ │ │ ├── approach.mdx │ │ │ └── icons.mdx │ │ ├── forms/ │ │ │ ├── checks-radios.mdx │ │ │ ├── floating-labels.mdx │ │ │ ├── form-control.mdx │ │ │ ├── input-group.mdx │ │ │ ├── layout.mdx │ │ │ ├── overview.mdx │ │ │ ├── range.mdx │ │ │ ├── select.mdx │ │ │ └── validation.mdx │ │ ├── getting-started/ │ │ │ ├── accessibility.mdx │ │ │ ├── best-practices.mdx │ │ │ ├── browsers-devices.mdx │ │ │ ├── contents.mdx │ │ │ ├── contribute.mdx │ │ │ ├── download.mdx │ │ │ ├── introduction.mdx │ │ │ ├── javascript.mdx │ │ │ ├── parcel.mdx │ │ │ ├── rfs.mdx │ │ │ ├── rtl.mdx │ │ │ ├── vite.mdx │ │ │ └── webpack.mdx │ │ ├── helpers/ │ │ │ ├── clearfix.mdx │ │ │ ├── color-background.mdx │ │ │ ├── colored-links.mdx │ │ │ ├── focus-ring.mdx │ │ │ ├── icon-link.mdx │ │ │ ├── position.mdx │ │ │ ├── ratio.mdx │ │ │ ├── stacks.mdx │ │ │ ├── stretched-link.mdx │ │ │ ├── text-truncation.mdx │ │ │ ├── vertical-rule.mdx │ │ │ └── visually-hidden.mdx │ │ ├── layout/ │ │ │ ├── breakpoints.mdx │ │ │ ├── columns.mdx │ │ │ ├── containers.mdx │ │ │ ├── css-grid.mdx │ │ │ ├── grid.mdx │ │ │ ├── gutters.mdx │ │ │ ├── utilities.mdx │ │ │ └── z-index.mdx │ │ ├── migration.mdx │ │ └── utilities/ │ │ ├── api.mdx │ │ ├── background.mdx │ │ ├── borders.mdx │ │ ├── colors.mdx │ │ ├── display.mdx │ │ ├── flex.mdx │ │ ├── float.mdx │ │ ├── interactions.mdx │ │ ├── link.mdx │ │ ├── object-fit.mdx │ │ ├── opacity.mdx │ │ ├── overflow.mdx │ │ ├── position.mdx │ │ ├── shadows.mdx │ │ ├── sizing.mdx │ │ ├── spacing.mdx │ │ ├── text.mdx │ │ ├── vertical-align.mdx │ │ ├── visibility.mdx │ │ └── z-index.mdx │ ├── env.d.ts │ ├── layouts/ │ │ ├── BaseLayout.astro │ │ ├── DocsLayout.astro │ │ ├── ExamplesLayout.astro │ │ ├── RedirectLayout.astro │ │ ├── SingleLayout.astro │ │ └── partials/ │ │ ├── ExamplesMain.astro │ │ ├── Icons.astro │ │ ├── ResponsiveImage.astro │ │ └── ThemeToggler.astro │ ├── libs/ │ │ ├── astro.ts │ │ ├── bootstrap.ts │ │ ├── config.ts │ │ ├── content.ts │ │ ├── data.ts │ │ ├── examples.ts │ │ ├── icon.ts │ │ ├── image.ts │ │ ├── layout.ts │ │ ├── path.ts │ │ ├── placeholder.ts │ │ ├── prism.ts │ │ ├── rehype.ts │ │ ├── remark.ts │ │ ├── toc.ts │ │ ├── utils.ts │ │ └── validation.ts │ ├── pages/ │ │ ├── 404.astro │ │ ├── [...alias].astro │ │ ├── docs/ │ │ │ ├── [version]/ │ │ │ │ ├── [...slug].astro │ │ │ │ ├── examples/ │ │ │ │ │ ├── [...asset].ts │ │ │ │ │ ├── [...example].astro │ │ │ │ │ └── index.astro │ │ │ │ └── index.astro │ │ │ ├── index.astro │ │ │ └── versions.astro │ │ ├── examples.astro │ │ ├── index.astro │ │ └── robots.txt.ts │ ├── plugins/ │ │ ├── algolia-plugin.js │ │ └── stackblitz-plugin.js │ ├── scss/ │ │ ├── _ads.scss │ │ ├── _anchor.scss │ │ ├── _brand.scss │ │ ├── _buttons.scss │ │ ├── _callouts.scss │ │ ├── _clipboard-js.scss │ │ ├── _colors.scss │ │ ├── _component-examples.scss │ │ ├── _content.scss │ │ ├── _footer.scss │ │ ├── _layout.scss │ │ ├── _masthead.scss │ │ ├── _navbar.scss │ │ ├── _placeholder-img.scss │ │ ├── _scrolling.scss │ │ ├── _search.scss │ │ ├── _sidebar.scss │ │ ├── _skippy.scss │ │ ├── _syntax.scss │ │ ├── _toc.scss │ │ ├── _variables.scss │ │ ├── docs.scss │ │ └── docs_search.scss │ └── types/ │ ├── auto-import.d.ts │ └── window.d.ts ├── static/ │ ├── CNAME │ ├── docs/ │ │ └── [version]/ │ │ └── assets/ │ │ ├── img/ │ │ │ └── favicons/ │ │ │ └── manifest.json │ │ └── js/ │ │ ├── color-modes.js │ │ └── validate-forms.js │ └── sw.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ module.exports = { presets: [ [ '@babel/preset-env', { loose: true, bugfixes: true, modules: false } ] ] }; ================================================ FILE: .browserslistrc ================================================ # https://github.com/browserslist/browserslist#readme >= 0.5% last 2 major versions not dead Chrome >= 60 Firefox >= 60 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) ================================================ FILE: .bundlewatch.config.json ================================================ { "files": [ { "path": "./dist/css/bootstrap-grid.css", "maxSize": "6.5 kB" }, { "path": "./dist/css/bootstrap-grid.min.css", "maxSize": "6.0 kB" }, { "path": "./dist/css/bootstrap-reboot.css", "maxSize": "3.5 kB" }, { "path": "./dist/css/bootstrap-reboot.min.css", "maxSize": "3.25 kB" }, { "path": "./dist/css/bootstrap-utilities.css", "maxSize": "11.75 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", "maxSize": "10.75 kB" }, { "path": "./dist/css/bootstrap.css", "maxSize": "32.5 kB" }, { "path": "./dist/css/bootstrap.min.css", "maxSize": "30.25 kB" }, { "path": "./dist/js/bootstrap.bundle.js", "maxSize": "43.0 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", "maxSize": "23.5 kB" }, { "path": "./dist/js/bootstrap.esm.js", "maxSize": "28.0 kB" }, { "path": "./dist/js/bootstrap.esm.min.js", "maxSize": "18.25 kB" }, { "path": "./dist/js/bootstrap.js", "maxSize": "28.75 kB" }, { "path": "./dist/js/bootstrap.min.js", "maxSize": "16.25 kB" } ], "ci": { "trackBranches": [ "main", "v4-dev" ] } } ================================================ FILE: .cspell.json ================================================ { "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 } ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintignore ================================================ **/*.min.js **/dist/ **/vendor/ /_site/ /site/public/ /js/coverage/ /site/static/sw.js /site/static/docs/**/assets/sw.js /site/layouts/partials/ ================================================ FILE: .eslintrc.json ================================================ { "root": true, "extends": [ "plugin:import/errors", "plugin:import/warnings", "plugin:unicorn/recommended", "xo", "xo/browser" ], "rules": { "arrow-body-style": "off", "capitalized-comments": "off", "comma-dangle": [ "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, { "MemberExpression": "off", "SwitchCase": 1 } ], "logical-assignment-operators": "off", "max-params": [ "warn", 5 ], "multiline-ternary": [ "error", "always-multiline" ], "new-cap": [ "error", { "properties": false } ], "no-console": "error", "no-negated-condition": "off", "object-curly-spacing": [ "error", "always" ], "operator-linebreak": [ "error", "after" ], "prefer-object-has-own": "off", "prefer-template": "error", "semi": [ "error", "never" ], "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-method-this-argument": "off", "unicorn/no-null": "off", "unicorn/no-typeof-undefined": "off", "unicorn/no-unused-properties": "error", "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-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" } } ] } ================================================ FILE: .gitattributes ================================================ # Enforce Unix newlines * text=auto eol=lf # Don't diff or textually merge source maps *.map binary bootstrap.css linguist-vendored=false bootstrap.js linguist-vendored=false ================================================ FILE: .github/CODEOWNERS ================================================ *.js @twbs/js-review *.css @twbs/css-review *.scss @twbs/css-review ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to Bootstrap 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. Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. ## Using the issue tracker The [issue tracker](https://github.com/twbs/bootstrap/issues) is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) 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), [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 respect the opinions of others. - 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. ## 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: - `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. - `css` - Issues stemming from our compiled CSS or source Sass files. - `docs` - Issues for improving or updating our documentation. - `examples` - Issues involving the example templates included in our docs. - `feature` - Issues asking for a new feature to be added, or an existing one to be extended or modified. New features require a minor version bump (e.g., `v3.0.0` to `v3.1.0`). - `build` - Issues with our build system, which is used to run all our tests, concatenate and compile source files, and more. - `help wanted` - Issues we need or would love help from the community to resolve. - `js` - Issues stemming from our compiled or source JavaScript files. - `meta` - Issues with the project itself or our GitHub repository. For a complete look at our labels, see the [project labels page](https://github.com/twbs/bootstrap/labels). ## Bug reports A bug is a _demonstrable problem_ that is caused by the code in the repository. 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. 1. **Use the GitHub issue search** — check if the issue has already been reported. 2. **Check if the issue has been fixed** — try to reproduce it using the latest `main` (or `v4-dev` branch if the issue is about v4) in the repository. 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example. 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 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 would you expect to be the outcome? All these details will help people to fix any potential bugs. Example: > Short and descriptive example bug report title > > A summary of the issue and the browser/OS environment in which it occurs. If > suitable, include the steps required to reproduce the bug. > > 1. This is the first step > 2. This is the second step > 3. Further steps, etc. > > `` - a link to the reduced test case > > Any other information you want to share that is relevant to the issue being > reported. This might include the lines of code that you have identified as > causing the bug, and potential solutions (and your opinions on their > merits). ### Reporting upstream browser bugs Sometimes bugs reported to us are actually caused by bugs in the browser(s) themselves, not bugs in Bootstrap per se. | Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes | | ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- | | 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 provide as much detail and context as possible. ## Pull requests Good pull requests—patches, improvements, new features—are a fantastic help. They should remain focused in scope and avoid containing unrelated 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 make a PR. Please adhere to the [coding guidelines](#code-guidelines) used throughout the project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). **Do not edit `bootstrap.css` or `bootstrap.js`, and do not commit any dist files (`dist/` or `js/dist`).** Those files are automatically generated by our build tools. You should 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 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 documentation source files and is managed separately by the Bootstrap Core Team. Adhering to the following process is the best way to get your work included in the project: 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes: ```bash # Clone your fork of the repo into the current directory git clone https://github.com//bootstrap.git # Navigate to the newly cloned directory cd bootstrap # Assign the original repo to a remote called "upstream" git remote add upstream https://github.com/twbs/bootstrap.git ``` 2. If you cloned a while ago, get the latest changes from upstream: ```bash git checkout main git pull upstream main ``` 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 ``` 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 [interactive rebase](https://help.github.com/articles/about-git-rebase/) feature to tidy up your commits before making them public. 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 ``` 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 ``` 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 includes code changes) and under the terms of the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/) (if it includes documentation changes). ## Code guidelines ### HTML [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 [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/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 - No semicolons (in client-side JS) - 2 spaces (no tabs) - strict mode - "Attractive" ### Checking coding style Run `npm run test` before committing to ensure your changes follow our coding standards. ## License 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. ================================================ FILE: .github/INCIDENT_RESPONSE.md ================================================ # 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._ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ 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 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Ask the community url: https://github.com/twbs/bootstrap/discussions/new about: Ask and discuss questions with other Bootstrap community members. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ 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 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### 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 ================================================ FILE: .github/SUPPORT.md ================================================ ### Bug reports See the [contributing guidelines](CONTRIBUTING.md) for sharing bug reports. ### How-to For general troubleshooting or help getting started: - 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. ================================================ FILE: .github/codeql/codeql-config.yml ================================================ name: "CodeQL config" paths-ignore: - dist ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly day: tuesday time: "12:00" timezone: Europe/Athens groups: github-actions: patterns: - "*" - package-ecosystem: npm directory: "/" labels: - dependencies - v5 schedule: interval: weekly day: tuesday time: "12:00" timezone: Europe/Athens versioning-strategy: increase rebase-strategy: disabled groups: production-dependencies: dependency-type: "production" development-dependencies: dependency-type: "development" ================================================ FILE: .github/release-drafter.yml ================================================ name-template: 'v$NEXT_MAJOR_VERSION' tag-template: 'v$NEXT_MAJOR_VERSION' prerelease: true exclude-labels: - 'skip-changelog' categories: - title: '❗ Breaking Changes' labels: - 'breaking-change' - title: '🚀 Highlights' labels: - 'release-highlight' - title: '🚀 Features' labels: - 'new-feature' - 'feature' - 'enhancement' - title: '🐛 Bug fixes' labels: - 'fix' - 'bugfix' - 'bug' - title: '⚡ Performance improvements' labels: - 'performance' - title: '🎨 CSS' labels: - 'css' - title: '☕️ JavaScript' labels: - 'js' - title: '📖 Docs' labels: - 'docs' - title: '🛠 Examples' labels: - 'examples' - title: '🌎 Accessibility' labels: - 'accessibility' - title: '🔧 Utility API' labels: - 'utility API' - 'utilities' - title: '🏭 Tests' labels: - 'tests' - title: '🧰 Misc' labels: - 'build' - 'meta' - 'chore' - 'CI' - title: '📦 Dependencies' labels: - 'dependencies' change-template: '- #$NUMBER: $TITLE' template: | ## Changes $CHANGES ================================================ FILE: .github/workflows/browserstack.yml ================================================ name: BrowserStack on: push: branches: - "**" - "!dependabot/**" workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: browserstack: runs-on: ubuntu-latest if: github.repository == 'twbs/bootstrap' timeout-minutes: 30 steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" cache: npm - name: Install npm dependencies run: npm ci - name: Run dist run: npm run dist - name: Run BrowserStack tests run: npm run js-test-cloud env: BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}" BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}" GITHUB_SHA: "${{ github.sha }}" ================================================ FILE: .github/workflows/bundlewatch.yml ================================================ name: Bundlewatch on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: bundlewatch: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" cache: npm - name: Install npm dependencies run: npm ci - name: Run dist run: npm run dist - name: Run bundlewatch run: npm run bundlewatch env: BUNDLEWATCH_GITHUB_TOKEN: "${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}" CI_BRANCH_BASE: main ================================================ FILE: .github/workflows/calibreapp-image-actions.yml ================================================ 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 }} ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: - main - v4-dev - "!dependabot/**" pull_request: branches: - main - v4-dev - "!dependabot/**" schedule: - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: config-file: ./.github/codeql/codeql-config.yml languages: "javascript" queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:javascript" ================================================ FILE: .github/workflows/cspell.yml ================================================ 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@9cd41bb518a24fefdafd9880cbab8f0ceba04d28 # v8.3.0 with: config: ".cspell.json" files: "**/*.{md,mdx}" inline: error incremental_files_only: false ================================================ FILE: .github/workflows/css.yml ================================================ name: CSS on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: css: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" cache: npm - name: Install npm dependencies run: npm ci - name: Build CSS run: npm run css - name: Run CSS tests run: npm run css-test ================================================ FILE: .github/workflows/docs.yml ================================================ name: Docs on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: docs: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" cache: npm - run: java -version - name: Install npm dependencies run: npm ci - name: Build docs run: npm run docs-build - name: Validate HTML run: npm run docs-vnu - name: Run linkinator uses: JustinBeckwith/linkinator-action@f62ba0c110a76effb2ee6022cc6ce4ab161085e3 # v2.4 with: paths: _site recurse: true verbosity: error skip: "^http://localhost" ================================================ FILE: .github/workflows/issue-close-require.yml ================================================ 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. ================================================ FILE: .github/workflows/issue-labeled.yml ================================================ 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. ================================================ FILE: .github/workflows/js.yml ================================================ name: JS Tests on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: run: permissions: # allow coverallsapp/github-action to create new checks issues and fetch code checks: write contents: read name: JS Tests runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ env.NODE }} cache: npm - name: Install npm dependencies run: npm ci - name: Run dist run: npm run js - name: Run JS tests run: npm run js-test - name: Run Coveralls 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" ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" cache: npm - name: Install npm dependencies run: npm ci - name: Lint run: npm run lint ================================================ FILE: .github/workflows/node-sass.yml ================================================ name: CSS (node-sass) on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 22 permissions: contents: read jobs: css: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "${{ env.NODE }}" - name: Build CSS with node-sass run: | 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 ================================================ FILE: .github/workflows/publish-nuget.yml ================================================ 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@12c57947e9458a5b976961b08ea0706a17dd71ae # v3.0.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" ================================================ FILE: .github/workflows/release-notes.yml ================================================ name: Release notes 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@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/scorecard.yml ================================================ # 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ # Ignore docs files /_site/ # Numerous always-ignore extensions *.diff *.err *.log *.orig *.rej *.swo *.swp *.vi *.zip *~ # OS or Editor folders ._* .cache .DS_Store .idea .project .settings .tmproj *.esproj *.sublime-project *.sublime-workspace nbproject Thumbs.db # Local Netlify folder .netlify # Komodo .komodotools *.komodoproject # Folders to ignore /dist-sass/ /js/coverage/ /node_modules/ # Site /site/dist /site/node_modules /site/.astro /site/public ================================================ FILE: .prettierignore ================================================ # 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 ================================================ FILE: .stylelintignore ================================================ **/*.min.css **/dist/ **/vendor/ /_site/ /site/public/ /js/coverage/ ================================================ FILE: .stylelintrc.json ================================================ { "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 } } ] } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "astro-build.astro-vscode", "dbaeumer.vscode-eslint", "EditorConfig.EditorConfig", "hossaini.bootstrap-intellisense", "streetsidesoftware.code-spell-checker", "stylelint.vscode-stylelint" ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.fixAll.stylelint": "always" }, "editor.renderWhitespace": "all", "scss.validate": false, "stylelint.enable": true, "stylelint.validate": ["scss"] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge 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 a positive environment for our community include: - 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 include: - 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 email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities 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. 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 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 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. **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][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 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) 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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Bootstrap logo

Bootstrap

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

Report bug · Request feature · Blog

## Bootstrap 5 Our default branch is for development of our Bootstrap 5 release. Head to the [`v4-dev` branch](https://github.com/twbs/bootstrap/tree/v4-dev) to view the readme, documentation, and source code for Bootstrap 4. ## Table of contents - [Quick start](#quick-start) - [Status](#status) - [What’s included](#whats-included) - [Bugs and feature requests](#bugs-and-feature-requests) - [Documentation](#documentation) - [Contributing](#contributing) - [Community](#community) - [Versioning](#versioning) - [Creators](#creators) - [Thanks](#thanks) - [Copyright and license](#copyright-and-license) ## Quick start Several quick start options are available: - [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@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.3/getting-started/introduction/) for information on the framework contents, templates, examples, and more. ## Status [![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) ![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/choose). ## Documentation 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://docsearch.algolia.com/). ### Running documentation locally 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 in your browser, and voilà. Learn more about using Astro by reading its [documentation](https://docs.astro.build/en/getting-started/). ### Documentation for previous releases You can find all our previous releases docs on . [Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. ## Contributing Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/main/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo). Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/main/.editorconfig) for easy use in common text editors. Read more and download plugins at . ## Community Get updates on Bootstrap’s development and chat with the project maintainers and community members. - Follow [@getbootstrap on X](https://x.com/getbootstrap). - Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.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. ## Versioning For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible. See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. ## Creators **Mark Otto** - - **Jacob Thornton** - - ## Thanks 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 Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)] [![OC sponsor 0](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website) [![OC sponsor 1](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website) [![OC sponsor 2](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website) [![OC sponsor 3](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website) [![OC sponsor 4](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website) [![OC sponsor 5](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website) [![OC sponsor 6](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website) [![OC sponsor 7](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website) [![OC sponsor 8](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website) [![OC sponsor 9](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website) ## Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)] [![Backers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers) ## Copyright and license 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/). ================================================ FILE: SECURITY.md ================================================ # Reporting Security Issues The Bootstrap team and community take security issues in Bootstrap seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. To report a security issue, email [security@getbootstrap.com](mailto:security@getbootstrap.com) and include the word "SECURITY" in the subject line. We'll endeavor to respond quickly, and will keep you updated throughout the process. ================================================ FILE: build/banner.mjs ================================================ 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 year = new Date().getFullYear() function getBanner(pluginFilename) { return `/*! * Bootstrap${pluginFilename ? ` ${pluginFilename}` : ''} v${pkg.version} (${pkg.homepage}) * Copyright 2011-${year} ${pkg.author} * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */` } export default getBanner ================================================ FILE: build/build-plugins.mjs ================================================ #!/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) } })() ================================================ FILE: build/change-version.mjs ================================================ #!/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)) ================================================ FILE: build/docs-prep.sh ================================================ #!/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}" ================================================ FILE: build/generate-sri.mjs ================================================ #!/usr/bin/env node /*! * Script to generate SRI hashes for use in our docs. * Remember to use the same vendor files as the CDN ones, * otherwise the hashes won't match! * * Copyright 2017-2026 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ 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 __dirname = path.dirname(fileURLToPath(import.meta.url)) sh.config.fatal = true const configFile = path.join(__dirname, '../config.yml') // Array of objects which holds the files to generate SRI hashes for. // `file` is the path from the root folder // `configPropertyName` is the config.yml variable's name of the file const files = [ { file: 'dist/css/bootstrap.min.css', configPropertyName: 'css_hash' }, { file: 'dist/css/bootstrap.rtl.min.css', configPropertyName: 'css_rtl_hash' }, { file: 'dist/js/bootstrap.min.js', configPropertyName: 'js_hash' }, { file: 'dist/js/bootstrap.bundle.min.js', configPropertyName: 'js_bundle_hash' }, { file: 'node_modules/@popperjs/core/dist/umd/popper.min.js', configPropertyName: 'popper_hash' } ] for (const { file, configPropertyName } of files) { fs.readFile(file, 'utf8', (error, data) => { if (error) { throw error } const algorithm = 'sha384' const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64') const integrity = `${algorithm}-${hash}` console.log(`${configPropertyName}: ${integrity}`) sh.sed('-i', new RegExp(`^(\\s+${configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile) }) } ================================================ FILE: build/postcss.config.mjs ================================================ 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' } } } ================================================ FILE: build/rollup.config.mjs ================================================ 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 __dirname = path.dirname(fileURLToPath(import.meta.url)) const BUNDLE = process.env.BUNDLE === 'true' const ESM = process.env.ESM === 'true' let destinationFile = `bootstrap${ESM ? '.esm' : ''}` const external = ['@popperjs/core'] const plugins = [ babel({ // Only transpile our source code exclude: 'node_modules/**', // Include the helpers in the bundle, at most one copy of each babelHelpers: 'bundled' }) ] const globals = { '@popperjs/core': 'Popper' } if (BUNDLE) { destinationFile += '.bundle' // Remove last entry in external array to bundle Popper external.pop() delete globals['@popperjs/core'] plugins.push( replace({ 'process.env.NODE_ENV': '"production"', preventAssignment: true }), nodeResolve() ) } const rollupConfig = { input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), output: { banner: banner(), file: path.resolve(__dirname, `../dist/js/${destinationFile}.js`), format: ESM ? 'esm' : 'umd', globals, generatedCode: 'es2015' }, external, plugins } if (!ESM) { rollupConfig.output.name = 'bootstrap' } export default rollupConfig ================================================ FILE: build/vnu-jar.mjs ================================================ #!/usr/bin/env node /*! * Script to run vnu-jar if Java is available. * Copyright 2017-2026 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ import { execFile, spawn } from 'node:child_process' import vnu from 'vnu-jar' execFile('java', ['-version'], (error, stdout, stderr) => { if (error) { console.error('Skipping vnu-jar test; Java is probably missing.') console.error(error) return } console.log('Running vnu-jar validation...') 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('') const button = document.querySelector('button') button.click() expect(document.querySelectorAll('.alert')).toHaveSize(0) }) it('should close an alert without instantiating it manually with the parent selector', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const button = document.querySelector('button') button.click() expect(document.querySelectorAll('.alert')).toHaveSize(0) }) }) describe('close', () => { it('should close an alert', () => { return new Promise(resolve => { const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) fixtureEl.innerHTML = '
' const alertEl = document.querySelector('.alert') const alert = new Alert(alertEl) alertEl.addEventListener('closed.bs.alert', () => { expect(document.querySelectorAll('.alert')).toHaveSize(0) expect(spy).not.toHaveBeenCalled() resolve() }) alert.close() }) }) it('should close alert with fade class', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const alertEl = document.querySelector('.alert') const alert = new Alert(alertEl) alertEl.addEventListener('transitionend', () => { expect().nothing() }) alertEl.addEventListener('closed.bs.alert', () => { expect(document.querySelectorAll('.alert')).toHaveSize(0) resolve() }) alert.close() }) }) it('should not remove alert if close event is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '
' const getAlert = () => document.querySelector('.alert') const alertEl = getAlert() const alert = new Alert(alertEl) alertEl.addEventListener('close.bs.alert', event => { event.preventDefault() setTimeout(() => { expect(getAlert()).not.toBeNull() resolve() }, 10) }) alertEl.addEventListener('closed.bs.alert', () => { reject(new Error('should not fire closed event')) }) alert.close() }) }) }) describe('dispose', () => { it('should dispose an alert', () => { fixtureEl.innerHTML = '
' const alertEl = document.querySelector('.alert') const alert = new Alert(alertEl) expect(Alert.getInstance(alertEl)).not.toBeNull() alert.dispose() expect(Alert.getInstance(alertEl)).toBeNull() }) }) describe('jQueryInterface', () => { it('should handle config passed and toggle existing alert', () => { fixtureEl.innerHTML = '
' const alertEl = fixtureEl.querySelector('.alert') const alert = new Alert(alertEl) const spy = spyOn(alert, 'close') jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] jQueryMock.fn.alert.call(jQueryMock, 'close') expect(spy).toHaveBeenCalled() }) it('should create new alert instance and call close', () => { fixtureEl.innerHTML = '
' const alertEl = fixtureEl.querySelector('.alert') jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] expect(Alert.getInstance(alertEl)).toBeNull() jQueryMock.fn.alert.call(jQueryMock, 'close') expect(fixtureEl.querySelector('.alert')).toBeNull() }) it('should just create an alert instance without calling close', () => { fixtureEl.innerHTML = '
' const alertEl = fixtureEl.querySelector('.alert') jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] jQueryMock.fn.alert.call(jQueryMock) expect(Alert.getInstance(alertEl)).not.toBeNull() expect(fixtureEl.querySelector('.alert')).not.toBeNull() }) it('should throw an error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.alert.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should throw an error on protected method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = '_getConfig' jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.alert.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return alert instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const alert = new Alert(div) expect(Alert.getInstance(div)).toEqual(alert) expect(Alert.getInstance(div)).toBeInstanceOf(Alert) }) it('should return null when there is no alert instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Alert.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return alert instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const alert = new Alert(div) expect(Alert.getOrCreateInstance(div)).toEqual(alert) expect(Alert.getInstance(div)).toEqual(Alert.getOrCreateInstance(div, {})) expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert) }) it('should return new instance when there is no alert instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Alert.getInstance(div)).toBeNull() expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert) }) }) }) ================================================ FILE: js/tests/unit/base-component.spec.js ================================================ import BaseComponent from '../../src/base-component.js' import EventHandler from '../../src/dom/event-handler.js' import { noop } from '../../src/util/index.js' import { clearFixture, getFixture } from '../helpers/fixture.js' class DummyClass extends BaseComponent { constructor(element) { super(element) EventHandler.on(this._element, `click${DummyClass.EVENT_KEY}`, noop) } static get NAME() { return 'dummy' } } describe('Base Component', () => { let fixtureEl const name = 'dummy' let element let instance const createInstance = () => { fixtureEl.innerHTML = '
' element = fixtureEl.querySelector('#foo') instance = new DummyClass(element) } beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('Static Methods', () => { describe('VERSION', () => { it('should return version', () => { expect(DummyClass.VERSION).toEqual(jasmine.any(String)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(DummyClass.DATA_KEY).toEqual(`bs.${name}`) }) }) describe('NAME', () => { it('should throw an Error if it is not initialized', () => { expect(() => { // eslint-disable-next-line no-unused-expressions BaseComponent.NAME }).toThrowError(Error) }) it('should return plugin NAME', () => { expect(DummyClass.NAME).toEqual(name) }) }) describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(DummyClass.EVENT_KEY).toEqual(`.bs.${name}`) }) }) }) describe('Public Methods', () => { describe('constructor', () => { it('should accept element, either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ '
', '
' ].join('') const el = fixtureEl.querySelector('#foo') const elInstance = new DummyClass(el) const selectorInstance = new DummyClass('#bar') expect(elInstance._element).toEqual(el) expect(selectorInstance._element).toEqual(fixtureEl.querySelector('#bar')) }) it('should not initialize and add element record to Data (caching), if argument `element` is not an HTML element', () => { fixtureEl.innerHTML = '' const el = fixtureEl.querySelector('#foo') const elInstance = new DummyClass(el) const selectorInstance = new DummyClass('#bar') expect(elInstance._element).not.toBeDefined() expect(selectorInstance._element).not.toBeDefined() }) }) describe('dispose', () => { it('should dispose an component', () => { createInstance() expect(DummyClass.getInstance(element)).not.toBeNull() instance.dispose() expect(DummyClass.getInstance(element)).toBeNull() expect(instance._element).toBeNull() }) it('should de-register element event listeners', () => { createInstance() const spy = spyOn(EventHandler, 'off') instance.dispose() expect(spy).toHaveBeenCalledWith(element, DummyClass.EVENT_KEY) }) }) describe('getInstance', () => { it('should return an instance', () => { createInstance() expect(DummyClass.getInstance(element)).toEqual(instance) expect(DummyClass.getInstance(element)).toBeInstanceOf(DummyClass) }) it('should accept element, either passed as a CSS selector, jQuery element, or DOM element', () => { createInstance() expect(DummyClass.getInstance('#foo')).toEqual(instance) expect(DummyClass.getInstance(element)).toEqual(instance) const fakejQueryObject = { 0: element, jquery: 'foo' } expect(DummyClass.getInstance(fakejQueryObject)).toEqual(instance) }) it('should return null when there is no instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(DummyClass.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return an instance', () => { createInstance() expect(DummyClass.getOrCreateInstance(element)).toEqual(instance) expect(DummyClass.getInstance(element)).toEqual(DummyClass.getOrCreateInstance(element, {})) expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass) }) it('should return new instance when there is no alert instance', () => { fixtureEl.innerHTML = '
' element = fixtureEl.querySelector('#foo') expect(DummyClass.getInstance(element)).toBeNull() expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass) }) }) }) }) ================================================ FILE: js/tests/unit/button.spec.js ================================================ import Button from '../../src/button.js' import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Button', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '' const buttonEl = fixtureEl.querySelector('[data-bs-toggle="button"]') const buttonBySelector = new Button('[data-bs-toggle="button"]') const buttonByElement = new Button(buttonEl) expect(buttonBySelector._element).toEqual(buttonEl) expect(buttonByElement._element).toEqual(buttonEl) }) describe('VERSION', () => { it('should return plugin version', () => { expect(Button.VERSION).toEqual(jasmine.any(String)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Button.DATA_KEY).toEqual('bs.button') }) }) describe('data-api', () => { it('should toggle active class on click', () => { fixtureEl.innerHTML = [ '', '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') const btnTestParent = fixtureEl.querySelector('.testParent') expect(btn).not.toHaveClass('active') btn.click() expect(btn).toHaveClass('active') btn.click() expect(btn).not.toHaveClass('active') divTest.click() expect(btnTestParent).toHaveClass('active') }) }) describe('toggle', () => { it('should toggle aria-pressed', () => { fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) expect(btnEl.getAttribute('aria-pressed')).toEqual('false') expect(btnEl).not.toHaveClass('active') button.toggle() expect(btnEl.getAttribute('aria-pressed')).toEqual('true') expect(btnEl).toHaveClass('active') }) }) describe('dispose', () => { it('should dispose a button', () => { fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) expect(Button.getInstance(btnEl)).not.toBeNull() button.dispose() expect(Button.getInstance(btnEl)).toBeNull() }) }) describe('jQueryInterface', () => { it('should handle config passed and toggle existing button', () => { fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) const spy = spyOn(button, 'toggle') jQueryMock.fn.button = Button.jQueryInterface jQueryMock.elements = [btnEl] jQueryMock.fn.button.call(jQueryMock, 'toggle') expect(spy).toHaveBeenCalled() }) it('should create new button instance and call toggle', () => { fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') jQueryMock.fn.button = Button.jQueryInterface jQueryMock.elements = [btnEl] jQueryMock.fn.button.call(jQueryMock, 'toggle') expect(Button.getInstance(btnEl)).not.toBeNull() expect(btnEl).toHaveClass('active') }) it('should just create a button instance without calling toggle', () => { fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') jQueryMock.fn.button = Button.jQueryInterface jQueryMock.elements = [btnEl] jQueryMock.fn.button.call(jQueryMock) expect(Button.getInstance(btnEl)).not.toBeNull() expect(btnEl).not.toHaveClass('active') }) }) describe('getInstance', () => { it('should return button instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const button = new Button(div) expect(Button.getInstance(div)).toEqual(button) expect(Button.getInstance(div)).toBeInstanceOf(Button) }) it('should return null when there is no button instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Button.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return button instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const button = new Button(div) expect(Button.getOrCreateInstance(div)).toEqual(button) expect(Button.getInstance(div)).toEqual(Button.getOrCreateInstance(div, {})) expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button) }) it('should return new instance when there is no button instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Button.getInstance(div)).toBeNull() expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button) }) }) }) ================================================ FILE: js/tests/unit/carousel.spec.js ================================================ import Carousel from '../../src/carousel.js' import EventHandler from '../../src/dom/event-handler.js' import { isRTL, noop } from '../../src/util/index.js' import Swipe from '../../src/util/swipe.js' import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Carousel', () => { const { Simulator, PointerEvent } = window const originWinPointerEvent = PointerEvent const supportPointerEvent = Boolean(PointerEvent) const cssStyleCarousel = '.carousel.pointer-event { touch-action: none; }' const stylesCarousel = document.createElement('style') stylesCarousel.type = 'text/css' stylesCarousel.append(document.createTextNode(cssStyleCarousel)) const clearPointerEvents = () => { window.PointerEvent = null } const restorePointerEvents = () => { window.PointerEvent = originWinPointerEvent } let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Carousel.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Carousel.Default).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Carousel.DATA_KEY).toEqual('bs.carousel') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('#myCarousel') const carouselBySelector = new Carousel('#myCarousel') const carouselByElement = new Carousel(carouselEl) expect(carouselBySelector._element).toEqual(carouselEl) expect(carouselByElement._element).toEqual(carouselEl) }) it('should start cycling if `ride`===`carousel`', () => { fixtureEl.innerHTML = '' const carousel = new Carousel('#myCarousel') expect(carousel._interval).not.toBeNull() }) it('should not start cycling if `ride`!==`carousel`', () => { fixtureEl.innerHTML = '' const carousel = new Carousel('#myCarousel') expect(carousel._interval).toBeNull() }) it('should go to next item if right arrow key is pressed', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, { keyboard: true }) const spy = spyOn(carousel, '_keydown').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', () => { expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) expect(spy).toHaveBeenCalled() resolve() }) const keydown = createEvent('keydown') keydown.key = 'ArrowRight' carouselEl.dispatchEvent(keydown) }) }) it('should ignore keyboard events if data-bs-keyboard=false', () => { fixtureEl.innerHTML = [ '' ].join('') const spy = spyOn(EventHandler, 'trigger').and.callThrough() const carouselEl = fixtureEl.querySelector('#myCarousel') // eslint-disable-next-line no-new new Carousel('#myCarousel') expect(spy).not.toHaveBeenCalledWith(carouselEl, 'keydown.bs.carousel', jasmine.any(Function)) }) it('should ignore mouse events if data-bs-pause=false', () => { fixtureEl.innerHTML = [ '' ].join('') const spy = spyOn(EventHandler, 'trigger').and.callThrough() const carouselEl = fixtureEl.querySelector('#myCarousel') // eslint-disable-next-line no-new new Carousel('#myCarousel') expect(spy).not.toHaveBeenCalledWith(carouselEl, 'hover.bs.carousel', jasmine.any(Function)) }) it('should go to previous item if left arrow key is pressed', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, { keyboard: true }) const spy = spyOn(carousel, '_keydown').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', () => { expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) expect(spy).toHaveBeenCalled() resolve() }) const keydown = createEvent('keydown') keydown.key = 'ArrowLeft' carouselEl.dispatchEvent(keydown) }) }) it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, { keyboard: true }) const spy = spyOn(carousel, '_keydown').and.callThrough() carouselEl.addEventListener('keydown', event => { expect(spy).toHaveBeenCalled() expect(event.defaultPrevented).toBeFalse() resolve() }) const keydown = createEvent('keydown') keydown.key = 'ArrowDown' carouselEl.dispatchEvent(keydown) }) }) it('should ignore keyboard events within s and ', ' ', ' ', ' ', ' ', '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const input = fixtureEl.querySelector('input') const textarea = fixtureEl.querySelector('textarea') const carousel = new Carousel(carouselEl, { keyboard: true }) const spyKeydown = spyOn(carousel, '_keydown').and.callThrough() const spySlide = spyOn(carousel, '_slide') const keydown = createEvent('keydown', { bubbles: true, cancelable: true }) keydown.key = 'ArrowRight' Object.defineProperty(keydown, 'target', { value: input, writable: true, configurable: true }) input.dispatchEvent(keydown) expect(spyKeydown).toHaveBeenCalled() expect(spySlide).not.toHaveBeenCalled() spyKeydown.calls.reset() spySlide.calls.reset() Object.defineProperty(keydown, 'target', { value: textarea }) textarea.dispatchEvent(keydown) expect(spyKeydown).toHaveBeenCalled() expect(spySlide).not.toHaveBeenCalled() }) it('should not slide if arrow key is pressed and carousel is sliding', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true for (const key of ['ArrowLeft', 'ArrowRight']) { const keydown = createEvent('keydown') keydown.key = key carouselEl.dispatchEvent(keydown) } expect(spy).not.toHaveBeenCalled() }) it('should wrap around from end to start when wrap option is true', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, { wrap: true }) const getActiveId = () => carouselEl.querySelector('.carousel-item.active').getAttribute('id') carouselEl.addEventListener('slid.bs.carousel', event => { const activeId = getActiveId() if (activeId === 'two') { carousel.next() return } if (activeId === 'three') { carousel.next() return } if (activeId === 'one') { // carousel wrapped around and slid from 3rd to 1st slide expect(activeId).toEqual('one') expect(event.from + 1).toEqual(3) resolve() } }) carousel.next() }) }) it('should stay at the start when the prev method is called and wrap is false', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const firstElement = fixtureEl.querySelector('#one') const carousel = new Carousel(carouselEl, { wrap: false }) carouselEl.addEventListener('slid.bs.carousel', () => { reject(new Error('carousel slid when it should not have slid')) }) carousel.prev() setTimeout(() => { expect(firstElement).toHaveClass('active') resolve() }, 10) }) }) it('should not add touch event listeners if touch = false', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const spy = spyOn(Carousel.prototype, '_addTouchEventListeners') const carousel = new Carousel(carouselEl, { touch: false }) expect(spy).not.toHaveBeenCalled() expect(carousel._swipeHelper).toBeNull() }) it('should not add touch event listeners if touch supported = false', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') spyOn(Swipe, 'isSupported').and.returnValue(false) const carousel = new Carousel(carouselEl) EventHandler.off(carouselEl, Carousel.EVENT_KEY) const spy = spyOn(carousel, '_addTouchEventListeners') carousel._addEventListeners() expect(spy).not.toHaveBeenCalled() expect(carousel._swipeHelper).toBeNull() }) it('should add touch event listeners by default', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') spyOn(Carousel.prototype, '_addTouchEventListeners') // Headless browser does not support touch events, so need to fake it // to test that touch events are add properly. document.documentElement.ontouchstart = noop const carousel = new Carousel(carouselEl) expect(carousel._addTouchEventListeners).toHaveBeenCalled() }) it('should allow swiperight and call _slide (prev) with pointer events', () => { return new Promise(resolve => { if (!supportPointerEvent) { expect().nothing() resolve() return } document.documentElement.ontouchstart = noop document.head.append(stylesCarousel) Simulator.setType('pointer') fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const item = fixtureEl.querySelector('#item') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, '_slide').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', event => { expect(item).toHaveClass('active') expect(spy).toHaveBeenCalledWith('prev') expect(event.direction).toEqual('right') stylesCarousel.remove() delete document.documentElement.ontouchstart resolve() }) Simulator.gestures.swipe(carouselEl, { deltaX: 300, deltaY: 0 }) }) }) it('should allow swipeleft and call next with pointer events', () => { return new Promise(resolve => { if (!supportPointerEvent) { expect().nothing() resolve() return } document.documentElement.ontouchstart = noop document.head.append(stylesCarousel) Simulator.setType('pointer') fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const item = fixtureEl.querySelector('#item') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, '_slide').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', event => { expect(item).not.toHaveClass('active') expect(spy).toHaveBeenCalledWith('next') expect(event.direction).toEqual('left') stylesCarousel.remove() delete document.documentElement.ontouchstart resolve() }) Simulator.gestures.swipe(carouselEl, { pos: [300, 10], deltaX: -300, deltaY: 0 }) }) }) it('should allow swiperight and call _slide (prev) with touch events', () => { return new Promise(resolve => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = noop fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const item = fixtureEl.querySelector('#item') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, '_slide').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', event => { expect(item).toHaveClass('active') expect(spy).toHaveBeenCalledWith('prev') expect(event.direction).toEqual('right') delete document.documentElement.ontouchstart restorePointerEvents() resolve() }) Simulator.gestures.swipe(carouselEl, { deltaX: 300, deltaY: 0 }) }) }) it('should allow swipeleft and call _slide (next) with touch events', () => { return new Promise(resolve => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = noop fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const item = fixtureEl.querySelector('#item') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, '_slide').and.callThrough() carouselEl.addEventListener('slid.bs.carousel', event => { expect(item).not.toHaveClass('active') expect(spy).toHaveBeenCalledWith('next') expect(event.direction).toEqual('left') delete document.documentElement.ontouchstart restorePointerEvents() resolve() }) Simulator.gestures.swipe(carouselEl, { pos: [300, 10], deltaX: -300, deltaY: 0 }) }) }) it('should not slide when swiping and carousel is sliding', () => { return new Promise(resolve => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = noop fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) carousel._isSliding = true const spy = spyOn(EventHandler, 'trigger') Simulator.gestures.swipe(carouselEl, { deltaX: 300, deltaY: 0 }) Simulator.gestures.swipe(carouselEl, { pos: [300, 10], deltaX: -300, deltaY: 0 }) setTimeout(() => { expect(spy).not.toHaveBeenCalled() delete document.documentElement.ontouchstart restorePointerEvents() resolve() }, 300) }) }) it('should not allow pinch with touch events', () => { return new Promise(resolve => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = noop fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) Simulator.gestures.swipe(carouselEl, { pos: [300, 10], deltaX: -300, deltaY: 0, touches: 2 }, () => { restorePointerEvents() delete document.documentElement.ontouchstart expect(carousel._swipeHelper._deltaX).toEqual(0) resolve() }) }) }) it('should call pause method on mouse over with pause equal to hover', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, 'pause') const mouseOverEvent = createEvent('mouseover') carouselEl.dispatchEvent(mouseOverEvent) setTimeout(() => { expect(spy).toHaveBeenCalled() resolve() }, 10) }) }) it('should call `maybeEnableCycle` on mouse out with pause equal to hover', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) const spyEnable = spyOn(carousel, '_maybeEnableCycle').and.callThrough() const spyCycle = spyOn(carousel, 'cycle') const mouseOutEvent = createEvent('mouseout') carouselEl.dispatchEvent(mouseOutEvent) setTimeout(() => { expect(spyEnable).toHaveBeenCalled() expect(spyCycle).toHaveBeenCalled() resolve() }, 10) }) }) }) describe('next', () => { it('should not slide if the carousel is sliding', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true carousel.next() expect(spy).not.toHaveBeenCalled() }) it('should not fire slid when slide is prevented', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) let slidEvent = false const doneTest = () => { setTimeout(() => { expect(slidEvent).toBeFalse() resolve() }, 20) } carouselEl.addEventListener('slide.bs.carousel', event => { event.preventDefault() doneTest() }) carouselEl.addEventListener('slid.bs.carousel', () => { slidEvent = true }) carousel.next() }) }) it('should fire slide event with: direction, relatedTarget, from and to', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) const onSlide = event => { expect(event.direction).toEqual('left') expect(event.relatedTarget).toHaveClass('carousel-item') expect(event.from).toEqual(0) expect(event.to).toEqual(1) carouselEl.removeEventListener('slide.bs.carousel', onSlide) carouselEl.addEventListener('slide.bs.carousel', onSlide2) carousel.prev() } const onSlide2 = event => { expect(event.direction).toEqual('right') resolve() } carouselEl.addEventListener('slide.bs.carousel', onSlide) carousel.next() }) }) it('should fire slid event with: direction, relatedTarget, from and to', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) const onSlid = event => { expect(event.direction).toEqual('left') expect(event.relatedTarget).toHaveClass('carousel-item') expect(event.from).toEqual(0) expect(event.to).toEqual(1) carouselEl.removeEventListener('slid.bs.carousel', onSlid) carouselEl.addEventListener('slid.bs.carousel', onSlid2) carousel.prev() } const onSlid2 = event => { expect(event.direction).toEqual('right') resolve() } carouselEl.addEventListener('slid.bs.carousel', onSlid) carousel.next() }) }) it('should update the active element to the next item before sliding', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const secondItemEl = fixtureEl.querySelector('#secondItem') const carousel = new Carousel(carouselEl) carousel.next() expect(carousel._activeElement).toEqual(secondItemEl) }) it('should continue cycling if it was already', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, 'cycle') carousel.next() expect(spy).not.toHaveBeenCalled() carousel.cycle() carousel.next() expect(spy).toHaveBeenCalledTimes(1) }) it('should update indicators if present', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const firstIndicator = fixtureEl.querySelector('#firstIndicator') const secondIndicator = fixtureEl.querySelector('#secondIndicator') const carousel = new Carousel(carouselEl) carouselEl.addEventListener('slid.bs.carousel', () => { expect(firstIndicator).not.toHaveClass('active') expect(firstIndicator.hasAttribute('aria-current')).toBeFalse() expect(secondIndicator).toHaveClass('active') expect(secondIndicator.getAttribute('aria-current')).toEqual('true') resolve() }) carousel.next() }) }) it('should call next()/prev() instance methods when clicking the respective direction buttons', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#carousel') const prevBtnEl = fixtureEl.querySelector('.carousel-control-prev') const nextBtnEl = fixtureEl.querySelector('.carousel-control-next') const carousel = new Carousel(carouselEl) const nextSpy = spyOn(carousel, 'next') const prevSpy = spyOn(carousel, 'prev') const spyEnable = spyOn(carousel, '_maybeEnableCycle') nextBtnEl.click() prevBtnEl.click() expect(nextSpy).toHaveBeenCalled() expect(prevSpy).toHaveBeenCalled() expect(spyEnable).toHaveBeenCalled() }) }) describe('nextWhenVisible', () => { it('should not call next when the page is not visible', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, 'next') carousel.nextWhenVisible() expect(spy).not.toHaveBeenCalled() }) }) describe('prev', () => { it('should not slide if the carousel is sliding', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true carousel.prev() expect(spy).not.toHaveBeenCalled() }) }) describe('pause', () => { it('should trigger transitionend if the carousel have carousel-item-next or carousel-item-prev class, cause is sliding', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl) const spy = spyOn(carousel, '_clearInterval') carouselEl.addEventListener('transitionend', () => { expect(spy).toHaveBeenCalled() resolve() }) carousel._slide('next') carousel.pause() }) }) }) describe('cycle', () => { it('should set an interval', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl) const spy = spyOn(window, 'setInterval').and.callThrough() carousel.cycle() expect(spy).toHaveBeenCalled() }) it('should clear interval if there is one', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl) carousel._interval = setInterval(noop, 10) const spySet = spyOn(window, 'setInterval').and.callThrough() const spyClear = spyOn(window, 'clearInterval').and.callThrough() carousel.cycle() expect(spySet).toHaveBeenCalled() expect(spyClear).toHaveBeenCalled() }) it('should get interval from data attribute on the active item element', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const secondItemEl = fixtureEl.querySelector('#secondItem') const carousel = new Carousel(carouselEl, { interval: 1814 }) expect(carousel._config.interval).toEqual(1814) carousel.cycle() expect(carousel._config.interval).toEqual(7) carousel._activeElement = secondItemEl carousel.cycle() expect(carousel._config.interval).toEqual(9385) }) }) describe('to', () => { it('should go directly to the provided index', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) carousel.to(2) carouselEl.addEventListener('slid.bs.carousel', () => { expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) resolve() }) }) }) it('should return to a previous slide if the provided index is lower than the current', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) carousel.to(1) carouselEl.addEventListener('slid.bs.carousel', () => { expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) resolve() }) }) }) it('should do nothing if a wrong index is provided', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(carousel, '_slide') carousel.to(25) expect(spy).not.toHaveBeenCalled() spy.calls.reset() carousel.to(-5) expect(spy).not.toHaveBeenCalled() }) it('should not continue if the provided is the same compare to the current one', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(carousel, '_slide') carousel.to(0) expect(spy).not.toHaveBeenCalled() }) it('should wait before performing to if a slide is sliding', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) const spyOne = spyOn(EventHandler, 'one').and.callThrough() const spySlide = spyOn(carousel, '_slide') carousel._isSliding = true carousel.to(1) expect(spySlide).not.toHaveBeenCalled() expect(spyOne).toHaveBeenCalled() const spyTo = spyOn(carousel, 'to') EventHandler.trigger(carouselEl, 'slid.bs.carousel') setTimeout(() => { expect(spyTo).toHaveBeenCalledWith(1) resolve() }) }) }) }) describe('rtl function', () => { it('"_directionToOrder" and "_orderToDirection" must return the right results', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) expect(carousel._directionToOrder('left')).toEqual('next') expect(carousel._directionToOrder('right')).toEqual('prev') expect(carousel._orderToDirection('next')).toEqual('left') expect(carousel._orderToDirection('prev')).toEqual('right') }) it('"_directionToOrder" and "_orderToDirection" must return the right results when rtl=true', () => { document.documentElement.dir = 'rtl' fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) expect(isRTL()).toBeTrue() expect(carousel._directionToOrder('left')).toEqual('prev') expect(carousel._directionToOrder('right')).toEqual('next') expect(carousel._orderToDirection('next')).toEqual('right') expect(carousel._orderToDirection('prev')).toEqual('left') document.documentElement.dir = 'ltl' }) it('"_slide" has to call _directionToOrder and "_orderToDirection"', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(carousel, '_orderToDirection').and.callThrough() carousel._slide(carousel._directionToOrder('left')) expect(spy).toHaveBeenCalledWith('next') carousel._slide(carousel._directionToOrder('right')) expect(spy).toHaveBeenCalledWith('prev') }) it('"_slide" has to call "_directionToOrder" and "_orderToDirection" when rtl=true', () => { document.documentElement.dir = 'rtl' fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) const spy = spyOn(carousel, '_orderToDirection').and.callThrough() carousel._slide(carousel._directionToOrder('left')) expect(spy).toHaveBeenCalledWith('prev') carousel._slide(carousel._directionToOrder('right')) expect(spy).toHaveBeenCalledWith('next') document.documentElement.dir = 'ltl' }) }) describe('dispose', () => { it('should destroy a carousel', () => { fixtureEl.innerHTML = [ '' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') const addEventSpy = spyOn(carouselEl, 'addEventListener').and.callThrough() const removeEventSpy = spyOn(EventHandler, 'off').and.callThrough() // Headless browser does not support touch events, so need to fake it // to test that touch events are add/removed properly. document.documentElement.ontouchstart = noop const carousel = new Carousel(carouselEl) const swipeHelperSpy = spyOn(carousel._swipeHelper, 'dispose').and.callThrough() const expectedArgs = [ ['keydown', jasmine.any(Function), jasmine.any(Boolean)], ['mouseover', jasmine.any(Function), jasmine.any(Boolean)], ['mouseout', jasmine.any(Function), jasmine.any(Boolean)], ...(carousel._swipeHelper._supportPointerEvents ? [ ['pointerdown', jasmine.any(Function), jasmine.any(Boolean)], ['pointerup', jasmine.any(Function), jasmine.any(Boolean)] ] : [ ['touchstart', jasmine.any(Function), jasmine.any(Boolean)], ['touchmove', jasmine.any(Function), jasmine.any(Boolean)], ['touchend', jasmine.any(Function), jasmine.any(Boolean)] ]) ] expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs) carousel.dispose() expect(carousel._swipeHelper).toBeNull() expect(removeEventSpy).toHaveBeenCalledWith(carouselEl, Carousel.EVENT_KEY) expect(swipeHelperSpy).toHaveBeenCalled() delete document.documentElement.ontouchstart }) }) describe('getInstance', () => { it('should return carousel instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const carousel = new Carousel(div) expect(Carousel.getInstance(div)).toEqual(carousel) expect(Carousel.getInstance(div)).toBeInstanceOf(Carousel) }) it('should return null when there is no carousel instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Carousel.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return carousel instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const carousel = new Carousel(div) expect(Carousel.getOrCreateInstance(div)).toEqual(carousel) expect(Carousel.getInstance(div)).toEqual(Carousel.getOrCreateInstance(div, {})) expect(Carousel.getOrCreateInstance(div)).toBeInstanceOf(Carousel) }) it('should return new instance when there is no carousel instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Carousel.getInstance(div)).toBeNull() expect(Carousel.getOrCreateInstance(div)).toBeInstanceOf(Carousel) }) it('should return new instance when there is no carousel instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Carousel.getInstance(div)).toBeNull() const carousel = Carousel.getOrCreateInstance(div, { interval: 1 }) expect(carousel).toBeInstanceOf(Carousel) expect(carousel._config.interval).toEqual(1) }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const carousel = new Carousel(div, { interval: 1 }) expect(Carousel.getInstance(div)).toEqual(carousel) const carousel2 = Carousel.getOrCreateInstance(div, { interval: 2 }) expect(carousel).toBeInstanceOf(Carousel) expect(carousel2).toEqual(carousel) expect(carousel2._config.interval).toEqual(1) }) }) describe('jQueryInterface', () => { it('should create a carousel', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.carousel = Carousel.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.carousel.call(jQueryMock) expect(Carousel.getInstance(div)).not.toBeNull() }) it('should not re create a carousel', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const carousel = new Carousel(div) jQueryMock.fn.carousel = Carousel.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.carousel.call(jQueryMock) expect(Carousel.getInstance(div)).toEqual(carousel) }) it('should call to if the config is a number', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const carousel = new Carousel(div) const slideTo = 2 const spy = spyOn(carousel, 'to') jQueryMock.fn.carousel = Carousel.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.carousel.call(jQueryMock, slideTo) expect(spy).toHaveBeenCalledWith(slideTo) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.carousel = Carousel.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.carousel.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('data-api', () => { it('should init carousels with data-bs-ride="carousel" on load', () => { fixtureEl.innerHTML = '
' const carouselEl = fixtureEl.querySelector('div') const loadEvent = createEvent('load') window.dispatchEvent(loadEvent) const carousel = Carousel.getInstance(carouselEl) expect(carousel._interval).not.toBeNull() }) it('should create carousel and go to the next slide on click (with real button controls)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const next = fixtureEl.querySelector('#next') const item2 = fixtureEl.querySelector('#item2') next.click() setTimeout(() => { expect(item2).toHaveClass('active') resolve() }, 10) }) }) it('should create carousel and go to the next slide on click (using links as controls)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const next = fixtureEl.querySelector('#next') const item2 = fixtureEl.querySelector('#item2') next.click() setTimeout(() => { expect(item2).toHaveClass('active') resolve() }, 10) }) }) it('should create carousel and go to the next slide on click with data-bs-slide-to', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const next = fixtureEl.querySelector('#next') const item2 = fixtureEl.querySelector('#item2') next.click() setTimeout(() => { expect(item2).toHaveClass('active') expect(Carousel.getInstance('#myCarousel')._interval).not.toBeNull() resolve() }, 10) }) }) it('should do nothing if no selector on click on arrows', () => { fixtureEl.innerHTML = [ '' ].join('') const next = fixtureEl.querySelector('#next') next.click() expect().nothing() }) it('should do nothing if no carousel class on click on arrows', () => { fixtureEl.innerHTML = [ '
', ' ', ' ', ' ', '
' ].join('') const next = fixtureEl.querySelector('#next') next.click() expect().nothing() }) }) }) ================================================ FILE: js/tests/unit/collapse.spec.js ================================================ import Collapse from '../../src/collapse.js' import EventHandler from '../../src/dom/event-handler.js' import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Collapse', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Collapse.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Collapse.Default).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Collapse.DATA_KEY).toEqual('bs.collapse') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div.my-collapse') const collapseBySelector = new Collapse('div.my-collapse') const collapseByElement = new Collapse(collapseEl) expect(collapseBySelector._element).toEqual(collapseEl) expect(collapseByElement._element).toEqual(collapseEl) }) it('should allow jquery object in parent config', () => { fixtureEl.innerHTML = [ '
', '
', ' Toggle item', '
Lorem ipsum
', '
', '
' ].join('') const collapseEl = fixtureEl.querySelector('div.collapse') const myCollapseEl = fixtureEl.querySelector('.my-collapse') const fakejQueryObject = { 0: myCollapseEl, jquery: 'foo' } const collapse = new Collapse(collapseEl, { parent: fakejQueryObject }) expect(collapse._config.parent).toEqual(myCollapseEl) }) it('should allow non jquery object in parent config', () => { fixtureEl.innerHTML = [ '
', '
', ' Toggle item', '
Lorem ipsum
', '
', '
' ].join('') const collapseEl = fixtureEl.querySelector('div.collapse') const myCollapseEl = fixtureEl.querySelector('.my-collapse') const collapse = new Collapse(collapseEl, { parent: myCollapseEl }) expect(collapse._config.parent).toEqual(myCollapseEl) }) it('should allow string selector in parent config', () => { fixtureEl.innerHTML = [ '
', '
', ' Toggle item', '
Lorem ipsum
', '
', '
' ].join('') const collapseEl = fixtureEl.querySelector('div.collapse') const myCollapseEl = fixtureEl.querySelector('.my-collapse') const collapse = new Collapse(collapseEl, { parent: 'div.my-collapse' }) expect(collapse._config.parent).toEqual(myCollapseEl) }) }) describe('toggle', () => { it('should call show method if show class is not present', () => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl) const spy = spyOn(collapse, 'show') collapse.toggle() expect(spy).toHaveBeenCalled() }) it('should call hide method if show class is present', () => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('.show') const collapse = new Collapse(collapseEl, { toggle: false }) const spy = spyOn(collapse, 'hide') collapse.toggle() expect(spy).toHaveBeenCalled() }) it('should find collapse children if they have collapse class too not only data-bs-parent', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' Toggle item 1', '
Lorem ipsum 1
', '
', '
', ' Toggle item 2', '
Lorem ipsum 2
', '
', '
' ].join('') const parent = fixtureEl.querySelector('.my-collapse') const collapseEl1 = fixtureEl.querySelector('#collapse1') const collapseEl2 = fixtureEl.querySelector('#collapse2') const collapseList = [].concat(...fixtureEl.querySelectorAll('.collapse')) .map(el => new Collapse(el, { parent, toggle: false })) collapseEl2.addEventListener('shown.bs.collapse', () => { expect(collapseEl2).toHaveClass('show') expect(collapseEl1).not.toHaveClass('show') resolve() }) collapseList[1].toggle() }) }) }) describe('show', () => { it('should do nothing if is transitioning', () => { fixtureEl.innerHTML = '
' const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapse._isTransitioning = true collapse.show() expect(spy).not.toHaveBeenCalled() }) it('should do nothing if already shown', () => { fixtureEl.innerHTML = '
' const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapse.show() expect(spy).not.toHaveBeenCalled() }) it('should show a collapsed element', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapseEl.addEventListener('show.bs.collapse', () => { expect(collapseEl.style.height).toEqual('0px') }) collapseEl.addEventListener('shown.bs.collapse', () => { expect(collapseEl).toHaveClass('show') expect(collapseEl.style.height).toEqual('') resolve() }) collapse.show() }) }) it('should show a collapsed element on width', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapseEl.addEventListener('show.bs.collapse', () => { expect(collapseEl.style.width).toEqual('0px') }) collapseEl.addEventListener('shown.bs.collapse', () => { expect(collapseEl).toHaveClass('show') expect(collapseEl.style.width).toEqual('') resolve() }) collapse.show() }) }) it('should collapse only the first collapse', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
', '
' ].join('') const el1 = fixtureEl.querySelector('#collapse1') const el2 = fixtureEl.querySelector('#collapse2') const collapse = new Collapse(el1, { toggle: false }) el1.addEventListener('shown.bs.collapse', () => { expect(el1).toHaveClass('show') expect(el2).toHaveClass('show') resolve() }) collapse.show() }) }) it('should be able to handle toggling of other children siblings', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' ', '
', '
', '
', '
', '
', '
', ' ', '
', '
', '
content
', '
', '
', '
', '
', ' ', '
', '
', '
content
', '
', '
', '
', '
', '
', '
' ].join('') const el = selector => fixtureEl.querySelector(selector) const parentBtn = el('[data-bs-target="#parentContent"]') const childBtn1 = el('[data-bs-target="#childContent1"]') const childBtn2 = el('[data-bs-target="#childContent2"]') const parentCollapseEl = el('#parentContent') const childCollapseEl1 = el('#childContent1') const childCollapseEl2 = el('#childContent2') parentCollapseEl.addEventListener('shown.bs.collapse', () => { expect(parentCollapseEl).toHaveClass('show') childBtn1.click() }) childCollapseEl1.addEventListener('shown.bs.collapse', () => { expect(childCollapseEl1).toHaveClass('show') childBtn2.click() }) childCollapseEl2.addEventListener('shown.bs.collapse', () => { expect(childCollapseEl2).toHaveClass('show') expect(childCollapseEl1).not.toHaveClass('show') resolve() }) parentBtn.click() }) }) it('should not change tab tabpanels descendants on accordion', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '

', ' ', '

', '
', '
', ' ', ' ', '
', '
', '
', '
' ].join('') const el = fixtureEl.querySelector('#collapseOne') const activeTabPane = fixtureEl.querySelector('#nav-home') const collapse = new Collapse(el) let times = 1 el.addEventListener('hidden.bs.collapse', () => { collapse.show() }) el.addEventListener('shown.bs.collapse', () => { expect(activeTabPane).toHaveClass('show') times++ if (times === 2) { resolve() } collapse.hide() }) collapse.show() }) }) it('should not fire shown when show is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) const expectEnd = () => { setTimeout(() => { expect().nothing() resolve() }, 10) } collapseEl.addEventListener('show.bs.collapse', event => { event.preventDefault() expectEnd() }) collapseEl.addEventListener('shown.bs.collapse', () => { reject(new Error('should not fire shown event')) }) collapse.show() }) }) }) describe('hide', () => { it('should do nothing if is transitioning', () => { fixtureEl.innerHTML = '
' const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapse._isTransitioning = true collapse.hide() expect(spy).not.toHaveBeenCalled() }) it('should do nothing if already shown', () => { fixtureEl.innerHTML = '
' const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapse.hide() expect(spy).not.toHaveBeenCalled() }) it('should hide a collapsed element', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) collapseEl.addEventListener('hidden.bs.collapse', () => { expect(collapseEl).not.toHaveClass('show') expect(collapseEl.style.height).toEqual('') resolve() }) collapse.hide() }) }) it('should not fire hidden when hide is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) const expectEnd = () => { setTimeout(() => { expect().nothing() resolve() }, 10) } collapseEl.addEventListener('hide.bs.collapse', event => { event.preventDefault() expectEnd() }) collapseEl.addEventListener('hidden.bs.collapse', () => { reject(new Error('should not fire hidden event')) }) collapse.hide() }) }) }) describe('dispose', () => { it('should destroy a collapse', () => { fixtureEl.innerHTML = '
' const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { toggle: false }) expect(Collapse.getInstance(collapseEl)).toEqual(collapse) collapse.dispose() expect(Collapse.getInstance(collapseEl)).toBeNull() }) }) describe('data-api', () => { it('should prevent url change if click on nested elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
' ].join('') const triggerEl = fixtureEl.querySelector('a') const nestedTriggerEl = fixtureEl.querySelector('#nested') const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() triggerEl.addEventListener('click', event => { expect(event.target.isEqualNode(nestedTriggerEl)).toBeTrue() expect(event.delegateTarget.isEqualNode(triggerEl)).toBeTrue() expect(spy).toHaveBeenCalled() resolve() }) nestedTriggerEl.click() }) }) it('should show multiple collapsed elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
' ].join('') const trigger = fixtureEl.querySelector('a') const collapse1 = fixtureEl.querySelector('#collapse1') const collapse2 = fixtureEl.querySelector('#collapse2') collapse2.addEventListener('shown.bs.collapse', () => { expect(trigger.getAttribute('aria-expanded')).toEqual('true') expect(trigger).not.toHaveClass('collapsed') expect(collapse1).toHaveClass('show') expect(collapse1).toHaveClass('show') resolve() }) trigger.click() }) }) it('should hide multiple collapsed elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
' ].join('') const trigger = fixtureEl.querySelector('a') const collapse1 = fixtureEl.querySelector('#collapse1') const collapse2 = fixtureEl.querySelector('#collapse2') collapse2.addEventListener('hidden.bs.collapse', () => { expect(trigger.getAttribute('aria-expanded')).toEqual('false') expect(trigger).toHaveClass('collapsed') expect(collapse1).not.toHaveClass('show') expect(collapse1).not.toHaveClass('show') resolve() }) trigger.click() }) }) it('should remove "collapsed" class from target when collapse is shown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
' ].join('') const link1 = fixtureEl.querySelector('#link1') const link2 = fixtureEl.querySelector('#link2') const collapseTest1 = fixtureEl.querySelector('#test1') collapseTest1.addEventListener('shown.bs.collapse', () => { expect(link1.getAttribute('aria-expanded')).toEqual('true') expect(link2.getAttribute('aria-expanded')).toEqual('true') expect(link1).not.toHaveClass('collapsed') expect(link2).not.toHaveClass('collapsed') resolve() }) link1.click() }) }) it('should add "collapsed" class to target when collapse is hidden', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
' ].join('') const link1 = fixtureEl.querySelector('#link1') const link2 = fixtureEl.querySelector('#link2') const collapseTest1 = fixtureEl.querySelector('#test1') collapseTest1.addEventListener('hidden.bs.collapse', () => { expect(link1.getAttribute('aria-expanded')).toEqual('false') expect(link2.getAttribute('aria-expanded')).toEqual('false') expect(link1).toHaveClass('collapsed') expect(link2).toHaveClass('collapsed') resolve() }) link1.click() }) }) it('should allow accordion to use children other than card', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' ', '
', '
', '
', ' ', '
', '
', '
' ].join('') const trigger = fixtureEl.querySelector('#linkTrigger') const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') const collapseOne = fixtureEl.querySelector('#collapseOne') const collapseTwo = fixtureEl.querySelector('#collapseTwo') collapseOne.addEventListener('shown.bs.collapse', () => { expect(collapseOne).toHaveClass('show') expect(collapseTwo).not.toHaveClass('show') collapseTwo.addEventListener('shown.bs.collapse', () => { expect(collapseOne).not.toHaveClass('show') expect(collapseTwo).toHaveClass('show') resolve() }) triggerTwo.click() }) trigger.click() }) }) it('should not prevent event for input', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
' ].join('') const target = fixtureEl.querySelector('input') const collapseEl = fixtureEl.querySelector('#collapsediv1') collapseEl.addEventListener('shown.bs.collapse', () => { expect(collapseEl).toHaveClass('show') expect(target.checked).toBeTrue() resolve() }) target.click() }) }) it('should allow accordion to contain nested elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', ' ', '
', '
', '
', '
', '
', ' ', '
', '
', '
', '
', '
' ].join('') const triggerEl = fixtureEl.querySelector('#linkTrigger') const triggerTwoEl = fixtureEl.querySelector('#linkTriggerTwo') const collapseOneEl = fixtureEl.querySelector('#collapseOne') const collapseTwoEl = fixtureEl.querySelector('#collapseTwo') collapseOneEl.addEventListener('shown.bs.collapse', () => { expect(collapseOneEl).toHaveClass('show') expect(triggerEl).not.toHaveClass('collapsed') expect(triggerEl.getAttribute('aria-expanded')).toEqual('true') expect(collapseTwoEl).not.toHaveClass('show') expect(triggerTwoEl).toHaveClass('collapsed') expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false') collapseTwoEl.addEventListener('shown.bs.collapse', () => { expect(collapseOneEl).not.toHaveClass('show') expect(triggerEl).toHaveClass('collapsed') expect(triggerEl.getAttribute('aria-expanded')).toEqual('false') expect(collapseTwoEl).toHaveClass('show') expect(triggerTwoEl).not.toHaveClass('collapsed') expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true') resolve() }) triggerTwoEl.click() }) triggerEl.click() }) }) it('should allow accordion to target multiple elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
', '
', '
', '
', '
' ].join('') const trigger = fixtureEl.querySelector('#linkTriggerOne') const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') const collapseOneOne = fixtureEl.querySelector('#collapseOneOne') const collapseOneTwo = fixtureEl.querySelector('#collapseOneTwo') const collapseTwoOne = fixtureEl.querySelector('#collapseTwoOne') const collapseTwoTwo = fixtureEl.querySelector('#collapseTwoTwo') const collapsedElements = { one: false, two: false } function firstTest() { expect(collapseOneOne).toHaveClass('show') expect(collapseOneTwo).toHaveClass('show') expect(collapseTwoOne).not.toHaveClass('show') expect(collapseTwoTwo).not.toHaveClass('show') triggerTwo.click() } function secondTest() { expect(collapseOneOne).not.toHaveClass('show') expect(collapseOneTwo).not.toHaveClass('show') expect(collapseTwoOne).toHaveClass('show') expect(collapseTwoTwo).toHaveClass('show') resolve() } collapseOneOne.addEventListener('shown.bs.collapse', () => { if (collapsedElements.one) { firstTest() } else { collapsedElements.one = true } }) collapseOneTwo.addEventListener('shown.bs.collapse', () => { if (collapsedElements.one) { firstTest() } else { collapsedElements.one = true } }) collapseTwoOne.addEventListener('shown.bs.collapse', () => { if (collapsedElements.two) { secondTest() } else { collapsedElements.two = true } }) collapseTwoTwo.addEventListener('shown.bs.collapse', () => { if (collapsedElements.two) { secondTest() } else { collapsedElements.two = true } }) trigger.click() }) }) it('should collapse accordion children but not nested accordion children', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' ', '
', '
', '
', ' ', '
', '
', '
', '
', '
', '
', ' ', '
', '
', '
' ].join('') const trigger = fixtureEl.querySelector('#linkTrigger') const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') const nestedTrigger = fixtureEl.querySelector('#nestedLinkTrigger') const collapseOne = fixtureEl.querySelector('#collapseOne') const collapseTwo = fixtureEl.querySelector('#collapseTwo') const nestedCollapseOne = fixtureEl.querySelector('#nestedCollapseOne') function handlerCollapseOne() { expect(collapseOne).toHaveClass('show') expect(collapseTwo).not.toHaveClass('show') expect(nestedCollapseOne).not.toHaveClass('show') nestedCollapseOne.addEventListener('shown.bs.collapse', handlerNestedCollapseOne) nestedTrigger.click() collapseOne.removeEventListener('shown.bs.collapse', handlerCollapseOne) } function handlerNestedCollapseOne() { expect(collapseOne).toHaveClass('show') expect(collapseTwo).not.toHaveClass('show') expect(nestedCollapseOne).toHaveClass('show') collapseTwo.addEventListener('shown.bs.collapse', () => { expect(collapseOne).not.toHaveClass('show') expect(collapseTwo).toHaveClass('show') expect(nestedCollapseOne).toHaveClass('show') resolve() }) triggerTwo.click() nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) } collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) trigger.click() }) }) it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '', '
', '
' ].join('') const trigger1 = fixtureEl.querySelector('#trigger1') const trigger2 = fixtureEl.querySelector('#trigger2') const trigger3 = fixtureEl.querySelector('#trigger3') const target1 = fixtureEl.querySelector('#test1') const target2 = fixtureEl.querySelector(`#${CSS.escape('0/my/id')}`) const target2Shown = () => { expect(trigger1).not.toHaveClass('collapsed') expect(trigger1.getAttribute('aria-expanded')).toEqual('true') expect(trigger2).not.toHaveClass('collapsed') expect(trigger2.getAttribute('aria-expanded')).toEqual('true') expect(trigger3).not.toHaveClass('collapsed') expect(trigger3.getAttribute('aria-expanded')).toEqual('true') target2.addEventListener('hidden.bs.collapse', () => { expect(trigger1).not.toHaveClass('collapsed') expect(trigger1.getAttribute('aria-expanded')).toEqual('true') expect(trigger2).toHaveClass('collapsed') expect(trigger2.getAttribute('aria-expanded')).toEqual('false') expect(trigger3).not.toHaveClass('collapsed') expect(trigger3.getAttribute('aria-expanded')).toEqual('true') target1.addEventListener('hidden.bs.collapse', () => { expect(trigger1).toHaveClass('collapsed') expect(trigger1.getAttribute('aria-expanded')).toEqual('false') expect(trigger2).toHaveClass('collapsed') expect(trigger2.getAttribute('aria-expanded')).toEqual('false') expect(trigger3).toHaveClass('collapsed') expect(trigger3.getAttribute('aria-expanded')).toEqual('false') resolve() }) trigger1.click() }) trigger2.click() } target2.addEventListener('shown.bs.collapse', target2Shown) trigger3.click() }) }) }) describe('jQueryInterface', () => { it('should create a collapse', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.collapse = Collapse.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.collapse.call(jQueryMock) expect(Collapse.getInstance(div)).not.toBeNull() }) it('should not re create a collapse', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const collapse = new Collapse(div) jQueryMock.fn.collapse = Collapse.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.collapse.call(jQueryMock) expect(Collapse.getInstance(div)).toEqual(collapse) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.collapse = Collapse.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.collapse.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return collapse instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const collapse = new Collapse(div) expect(Collapse.getInstance(div)).toEqual(collapse) expect(Collapse.getInstance(div)).toBeInstanceOf(Collapse) }) it('should return null when there is no collapse instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Collapse.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return collapse instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const collapse = new Collapse(div) expect(Collapse.getOrCreateInstance(div)).toEqual(collapse) expect(Collapse.getInstance(div)).toEqual(Collapse.getOrCreateInstance(div, {})) expect(Collapse.getOrCreateInstance(div)).toBeInstanceOf(Collapse) }) it('should return new instance when there is no collapse instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Collapse.getInstance(div)).toBeNull() expect(Collapse.getOrCreateInstance(div)).toBeInstanceOf(Collapse) }) it('should return new instance when there is no collapse instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Collapse.getInstance(div)).toBeNull() const collapse = Collapse.getOrCreateInstance(div, { toggle: false }) expect(collapse).toBeInstanceOf(Collapse) expect(collapse._config.toggle).toBeFalse() }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const collapse = new Collapse(div, { toggle: false }) expect(Collapse.getInstance(div)).toEqual(collapse) const collapse2 = Collapse.getOrCreateInstance(div, { toggle: true }) expect(collapse).toBeInstanceOf(Collapse) expect(collapse2).toEqual(collapse) expect(collapse2._config.toggle).toBeFalse() }) }) }) ================================================ FILE: js/tests/unit/dom/data.spec.js ================================================ import Data from '../../../src/dom/data.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Data', () => { const TEST_KEY = 'bs.test' const UNKNOWN_KEY = 'bs.unknown' const TEST_DATA = { test: 'bsData' } let fixtureEl let div beforeAll(() => { fixtureEl = getFixture() }) beforeEach(() => { fixtureEl.innerHTML = '
' div = fixtureEl.querySelector('div') }) afterEach(() => { Data.remove(div, TEST_KEY) clearFixture() }) it('should return null for unknown elements', () => { const data = { ...TEST_DATA } Data.set(div, TEST_KEY, data) expect(Data.get(null)).toBeNull() expect(Data.get(undefined)).toBeNull() expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull() }) it('should return null for unknown keys', () => { const data = { ...TEST_DATA } Data.set(div, TEST_KEY, data) expect(Data.get(div, null)).toBeNull() expect(Data.get(div, undefined)).toBeNull() expect(Data.get(div, UNKNOWN_KEY)).toBeNull() }) it('should store data for an element with a given key and return it', () => { const data = { ...TEST_DATA } Data.set(div, TEST_KEY, data) expect(Data.get(div, TEST_KEY)).toEqual(data) }) it('should overwrite data if something is already stored', () => { const data = { ...TEST_DATA } const copy = { ...data } Data.set(div, TEST_KEY, data) Data.set(div, TEST_KEY, copy) // Using `toBe` since spread creates a shallow copy expect(Data.get(div, TEST_KEY)).not.toBe(data) expect(Data.get(div, TEST_KEY)).toBe(copy) }) it('should do nothing when an element has nothing stored', () => { Data.remove(div, TEST_KEY) expect().nothing() }) it('should remove nothing for an unknown key', () => { const data = { ...TEST_DATA } Data.set(div, TEST_KEY, data) Data.remove(div, UNKNOWN_KEY) expect(Data.get(div, TEST_KEY)).toEqual(data) }) it('should remove data for a given key', () => { const data = { ...TEST_DATA } Data.set(div, TEST_KEY, data) Data.remove(div, TEST_KEY) expect(Data.get(div, TEST_KEY)).toBeNull() }) it('should console.error a message if called with multiple keys', () => { console.error = jasmine.createSpy('console.error') const data = { ...TEST_DATA } const copy = { ...data } Data.set(div, TEST_KEY, data) Data.set(div, UNKNOWN_KEY, copy) expect(console.error).toHaveBeenCalled() expect(Data.get(div, UNKNOWN_KEY)).toBeNull() }) }) ================================================ FILE: js/tests/unit/dom/event-handler.spec.js ================================================ import EventHandler from '../../../src/dom/event-handler.js' import { noop } from '../../../src/util/index.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('EventHandler', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('on', () => { it('should not add event listener if the event is not a string', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') EventHandler.on(div, null, noop) EventHandler.on(null, 'click', noop) expect().nothing() }) it('should add event listener', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') EventHandler.on(div, 'click', () => { expect().nothing() resolve() }) div.click() }) }) it('should add namespaced event listener', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') EventHandler.on(div, 'bs.namespace', () => { expect().nothing() resolve() }) EventHandler.trigger(div, 'bs.namespace') }) }) it('should add native namespaced event listener', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') EventHandler.on(div, 'click.namespace', () => { expect().nothing() resolve() }) EventHandler.trigger(div, 'click') }) }) it('should handle event delegation', () => { return new Promise(resolve => { EventHandler.on(document, 'click', '.test', () => { expect().nothing() resolve() }) fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') div.click() }) }) it('should handle mouseenter/mouseleave like the native counterpart', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
', '
', '
', '
' ].join('') const outer = fixtureEl.querySelector('.outer') const inner = fixtureEl.querySelector('.inner') const nested = fixtureEl.querySelector('.nested') const deep = fixtureEl.querySelector('.deep') const sibling = fixtureEl.querySelector('.sibling') const enterSpy = jasmine.createSpy('mouseenter') const leaveSpy = jasmine.createSpy('mouseleave') const delegateEnterSpy = jasmine.createSpy('mouseenter') const delegateLeaveSpy = jasmine.createSpy('mouseleave') EventHandler.on(inner, 'mouseenter', enterSpy) EventHandler.on(inner, 'mouseleave', leaveSpy) EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy) EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy) EventHandler.on(sibling, 'mouseenter', () => { expect(enterSpy.calls.count()).toEqual(2) expect(leaveSpy.calls.count()).toEqual(2) expect(delegateEnterSpy.calls.count()).toEqual(2) expect(delegateLeaveSpy.calls.count()).toEqual(2) resolve() }) const moveMouse = (from, to) => { from.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, relatedTarget: to })) to.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, relatedTarget: from })) } // from outer to deep and back to outer (nested) moveMouse(outer, inner) moveMouse(inner, nested) moveMouse(nested, deep) moveMouse(deep, nested) moveMouse(nested, inner) moveMouse(inner, outer) setTimeout(() => { expect(enterSpy.calls.count()).toEqual(1) expect(leaveSpy.calls.count()).toEqual(1) expect(delegateEnterSpy.calls.count()).toEqual(1) expect(delegateLeaveSpy.calls.count()).toEqual(1) // from outer to inner to sibling (adjacent) moveMouse(outer, inner) moveMouse(inner, sibling) }, 20) }) }) }) describe('one', () => { it('should call listener just once', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' let called = 0 const div = fixtureEl.querySelector('div') const obj = { oneListener() { called++ } } EventHandler.one(div, 'bootstrap', obj.oneListener) EventHandler.trigger(div, 'bootstrap') EventHandler.trigger(div, 'bootstrap') setTimeout(() => { expect(called).toEqual(1) resolve() }, 20) }) }) it('should call delegated listener just once', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' let called = 0 const div = fixtureEl.querySelector('div') const obj = { oneListener() { called++ } } EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener) EventHandler.trigger(div, 'bootstrap') EventHandler.trigger(div, 'bootstrap') setTimeout(() => { expect(called).toEqual(1) resolve() }, 20) }) }) }) describe('off', () => { it('should not remove a listener', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') EventHandler.off(div, null, noop) EventHandler.off(null, 'click', noop) expect().nothing() }) it('should remove a listener', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let called = 0 const handler = () => { called++ } EventHandler.on(div, 'foobar', handler) EventHandler.trigger(div, 'foobar') EventHandler.off(div, 'foobar', handler) EventHandler.trigger(div, 'foobar') setTimeout(() => { expect(called).toEqual(1) resolve() }, 20) }) }) it('should remove all the events', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let called = 0 EventHandler.on(div, 'foobar', () => { called++ }) EventHandler.on(div, 'foobar', () => { called++ }) EventHandler.trigger(div, 'foobar') EventHandler.off(div, 'foobar') EventHandler.trigger(div, 'foobar') setTimeout(() => { expect(called).toEqual(2) resolve() }, 20) }) }) it('should remove all the namespaced listeners if namespace is passed', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let called = 0 EventHandler.on(div, 'foobar.namespace', () => { called++ }) EventHandler.on(div, 'foofoo.namespace', () => { called++ }) EventHandler.trigger(div, 'foobar.namespace') EventHandler.trigger(div, 'foofoo.namespace') EventHandler.off(div, '.namespace') EventHandler.trigger(div, 'foobar.namespace') EventHandler.trigger(div, 'foofoo.namespace') setTimeout(() => { expect(called).toEqual(2) resolve() }, 20) }) }) it('should remove the namespaced listeners', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let calledCallback1 = 0 let calledCallback2 = 0 EventHandler.on(div, 'foobar.namespace', () => { calledCallback1++ }) EventHandler.on(div, 'foofoo.namespace', () => { calledCallback2++ }) EventHandler.trigger(div, 'foobar.namespace') EventHandler.off(div, 'foobar.namespace') EventHandler.trigger(div, 'foobar.namespace') EventHandler.trigger(div, 'foofoo.namespace') setTimeout(() => { expect(calledCallback1).toEqual(1) expect(calledCallback2).toEqual(1) resolve() }, 20) }) }) it('should remove the all the namespaced listeners for native events', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let called = 0 EventHandler.on(div, 'click.namespace', () => { called++ }) EventHandler.on(div, 'click.namespace2', () => { called++ }) EventHandler.trigger(div, 'click') EventHandler.off(div, 'click') EventHandler.trigger(div, 'click') setTimeout(() => { expect(called).toEqual(2) resolve() }, 20) }) }) it('should remove the specified namespaced listeners for native events', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') let called1 = 0 let called2 = 0 EventHandler.on(div, 'click.namespace', () => { called1++ }) EventHandler.on(div, 'click.namespace2', () => { called2++ }) EventHandler.trigger(div, 'click') EventHandler.off(div, 'click.namespace') EventHandler.trigger(div, 'click') setTimeout(() => { expect(called1).toEqual(1) expect(called2).toEqual(2) resolve() }, 20) }) }) it('should remove a listener registered by .one', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const handler = () => { reject(new Error('called')) } EventHandler.one(div, 'foobar', handler) EventHandler.off(div, 'foobar', handler) EventHandler.trigger(div, 'foobar') setTimeout(() => { expect().nothing() resolve() }, 20) }) }) it('should remove the correct delegated event listener', () => { const element = document.createElement('div') const subelement = document.createElement('span') element.append(subelement) const anchor = document.createElement('a') element.append(anchor) let i = 0 const handler = () => { i++ } EventHandler.on(element, 'click', 'a', handler) EventHandler.on(element, 'click', 'span', handler) fixtureEl.append(element) EventHandler.trigger(anchor, 'click') EventHandler.trigger(subelement, 'click') // first listeners called expect(i).toEqual(2) EventHandler.off(element, 'click', 'span', handler) EventHandler.trigger(subelement, 'click') // removed listener not called expect(i).toEqual(2) EventHandler.trigger(anchor, 'click') // not removed listener called expect(i).toEqual(3) EventHandler.on(element, 'click', 'span', handler) EventHandler.trigger(anchor, 'click') EventHandler.trigger(subelement, 'click') // listener re-registered expect(i).toEqual(5) EventHandler.off(element, 'click', 'span') EventHandler.trigger(subelement, 'click') // listener removed again expect(i).toEqual(5) }) }) describe('general functionality', () => { it('should hydrate properties, and make them configurable', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
' ].join('') const div1 = fixtureEl.querySelector('#div1') const div2 = fixtureEl.querySelector('#div2') EventHandler.on(div1, 'click', event => { expect(event.currentTarget).toBe(div2) expect(event.delegateTarget).toBe(div1) expect(event.originalTarget).toBeNull() Object.defineProperty(event, 'currentTarget', { configurable: true, get() { return div1 } }) expect(event.currentTarget).toBe(div1) resolve() }) expect(() => { EventHandler.trigger(div1, 'click', { originalTarget: null, currentTarget: div2 }) }).not.toThrowError(TypeError) }) }) }) }) ================================================ FILE: js/tests/unit/dom/manipulator.spec.js ================================================ import Manipulator from '../../../src/dom/manipulator.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Manipulator', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('setDataAttribute', () => { it('should set data attribute prefixed with bs', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') Manipulator.setDataAttribute(div, 'key', 'value') expect(div.getAttribute('data-bs-key')).toEqual('value') }) it('should set data attribute in kebab case', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') Manipulator.setDataAttribute(div, 'testKey', 'value') expect(div.getAttribute('data-bs-test-key')).toEqual('value') }) }) describe('removeDataAttribute', () => { it('should only remove bs-prefixed data attribute', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') Manipulator.removeDataAttribute(div, 'key') expect(div.getAttribute('data-bs-key')).toBeNull() expect(div.getAttribute('data-key-bs')).toEqual('postfixed') expect(div.getAttribute('data-key')).toEqual('value') }) it('should remove data attribute in kebab case', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') Manipulator.removeDataAttribute(div, 'testKey') expect(div.getAttribute('data-bs-test-key')).toBeNull() }) }) describe('getDataAttributes', () => { it('should return an empty object for null', () => { expect(Manipulator.getDataAttributes(null)).toEqual({}) expect().nothing() }) it('should get only bs-prefixed data attributes without bs namespace', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttributes(div)).toEqual({ toggle: 'tabs', target: '#element' }) }) it('should omit `bs-config` data attribute', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttributes(div)).toEqual({ toggle: 'tabs', target: '#element' }) }) }) describe('getDataAttribute', () => { it('should only get bs-prefixed data attribute', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttribute(div, 'key')).toEqual('value') expect(Manipulator.getDataAttribute(div, 'test')).toBeNull() expect(Manipulator.getDataAttribute(div, 'toggle')).toBeNull() }) it('should get data attribute in kebab case', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttribute(div, 'testKey')).toEqual('value') }) it('should normalize data', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttribute(div, 'test')).toBeFalse() div.setAttribute('data-bs-test', 'true') expect(Manipulator.getDataAttribute(div, 'test')).toBeTrue() div.setAttribute('data-bs-test', '1') expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1) }) it('should normalize json data', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Manipulator.getDataAttribute(div, 'test')).toEqual({ delay: { show: 100, hide: 10 } }) const objectData = { 'Super Hero': ['Iron Man', 'Super Man'], testNum: 90, url: 'http://localhost:8080/test?foo=bar' } const dataStr = JSON.stringify(objectData) div.setAttribute('data-bs-test', encodeURIComponent(dataStr)) expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData) div.setAttribute('data-bs-test', dataStr) expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData) }) }) }) ================================================ FILE: js/tests/unit/dom/selector-engine.spec.js ================================================ import SelectorEngine from '../../../src/dom/selector-engine.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('SelectorEngine', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('find', () => { it('should find elements', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(SelectorEngine.find('div', fixtureEl)).toEqual([div]) }) it('should find elements globally', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('#test') expect(SelectorEngine.find('#test')).toEqual([div]) }) it('should handle :scope selectors', () => { fixtureEl.innerHTML = [ '
    ', '
  • ', '
  • ', ' link', '
  • ', '
  • ', '
' ].join('') const listEl = fixtureEl.querySelector('ul') const aActive = fixtureEl.querySelector('.active') expect(SelectorEngine.find(':scope > li > .active', listEl)).toEqual([aActive]) }) }) describe('findOne', () => { it('should return one element', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('#test') expect(SelectorEngine.findOne('#test')).toEqual(div) }) }) describe('children', () => { it('should find children', () => { fixtureEl.innerHTML = [ '
    ', '
  • ', '
  • ', '
  • ', '
' ].join('') const list = fixtureEl.querySelector('ul') const liList = [].concat(...fixtureEl.querySelectorAll('li')) const result = SelectorEngine.children(list, 'li') expect(result).toEqual(liList) }) }) describe('parents', () => { it('should return parents', () => { expect(SelectorEngine.parents(fixtureEl, 'body')).toHaveSize(1) }) }) describe('prev', () => { it('should return previous element', () => { fixtureEl.innerHTML = '
' const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) }) it('should return previous element with an extra element between', () => { fixtureEl.innerHTML = [ '
', '', '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) }) it('should return previous element with comments or text nodes between', () => { fixtureEl.innerHTML = [ '
', '
', '', 'Text', '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelectorAll('.test')[1] expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) }) }) describe('next', () => { it('should return next element', () => { fixtureEl.innerHTML = '
' const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) }) it('should return next element with an extra element between', () => { fixtureEl.innerHTML = [ '
', '', '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) }) it('should return next element with comments or text nodes between', () => { fixtureEl.innerHTML = [ '
', '', 'Text', '', '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) }) }) describe('focusableChildren', () => { it('should return only elements with specific tag names', () => { fixtureEl.innerHTML = [ '
lorem
', 'lorem', 'lorem', '', '', '', '', '
lorem
' ].join('') const expectedElements = [ fixtureEl.querySelector('a'), fixtureEl.querySelector('button'), fixtureEl.querySelector('input'), fixtureEl.querySelector('textarea'), fixtureEl.querySelector('select'), fixtureEl.querySelector('details') ] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) it('should return any element with non negative tab index', () => { fixtureEl.innerHTML = [ '
lorem
', '
lorem
', '
lorem
' ].join('') const expectedElements = [ fixtureEl.querySelector('[tabindex]'), fixtureEl.querySelector('[tabindex="0"]'), fixtureEl.querySelector('[tabindex="10"]') ] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) it('should return not return elements with negative tab index', () => { fixtureEl.innerHTML = '' const expectedElements = [] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) it('should return contenteditable elements', () => { fixtureEl.innerHTML = '
lorem
' const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) it('should not return disabled elements', () => { fixtureEl.innerHTML = '' const expectedElements = [] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) it('should not return invisible elements', () => { 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) }) }) }) ================================================ FILE: js/tests/unit/dropdown.spec.js ================================================ 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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Dropdown.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Dropdown.Default).toEqual(jasmine.any(Object)) }) }) describe('DefaultType', () => { it('should return plugin default type config', () => { expect(Dropdown.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Dropdown.DATA_KEY).toEqual('bs.dropdown') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]') const dropdownByElement = new Dropdown(btnDropdown) expect(dropdownBySelector._element).toEqual(btnDropdown) expect(dropdownByElement._element).toEqual(btnDropdown) }) it('should work on invalid markup', () => { return new Promise(resolve => { // TODO: REMOVE in v6 fixtureEl.innerHTML = [ '' ].join('') 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() expect(typeof offset).toEqual('function') dropdown.show() }) }) it('should create offset modifier correctly when offset option is a string into data attribute', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) expect(dropdown._getOffset()).toEqual([10, 20]) }) it('should allow to pass config to Popper with `popperConfig`', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown, { popperConfig: { placement: 'left' } }) const popperConfig = dropdown._getPopperConfig() expect(popperConfig.placement).toEqual('left') }) it('should allow to pass config to Popper with `popperConfig` as a function', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' }) const dropdown = new Dropdown(btnDropdown, { popperConfig: getPopperConfig }) const popperConfig = dropdown._getPopperConfig() // Ensure that the function was called with the default config. expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({ placement: jasmine.any(String) })) expect(popperConfig.placement).toEqual('left') }) }) describe('toggle', () => { it('should toggle a dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should destroy old popper references on toggle', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '' ].join('') const btnDropdown1 = fixtureEl.querySelector('.firstBtn') const btnDropdown2 = fixtureEl.querySelector('.secondBtn') const firstDropdownEl = fixtureEl.querySelector('.first') const secondDropdownEl = fixtureEl.querySelector('.second') const dropdown1 = new Dropdown(btnDropdown1) firstDropdownEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown1).toHaveClass('show') spyOn(dropdown1._popper, 'destroy') btnDropdown2.click() }) secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { expect(dropdown1._popper.destroy).toHaveBeenCalled() resolve() })) dropdown1.toggle() }) }) it('should toggle a dropdown and add/remove event listener on mobile', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const defaultValueOnTouchStart = document.documentElement.ontouchstart const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) document.documentElement.ontouchstart = noop const spy = spyOn(EventHandler, 'on') const spyOff = spyOn(EventHandler, 'off') btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) dropdown.toggle() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown).not.toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(spyOff).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) document.documentElement.ontouchstart = defaultValueOnTouchStart resolve() }) dropdown.toggle() }) }) it('should toggle a dropdown at the right', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a centered dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropup', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropupEl = fixtureEl.querySelector('.dropup') const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropup centered', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropupEl = fixtureEl.querySelector('.dropup-center') const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropup at the right', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropupEl = fixtureEl.querySelector('.dropup') const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropend', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropendEl = fixtureEl.querySelector('.dropend') const dropdown = new Dropdown(btnDropdown) dropendEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropstart', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropstartEl = fixtureEl.querySelector('.dropstart') const dropdown = new Dropdown(btnDropdown) dropstartEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropdown with parent reference', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown, { reference: 'parent' }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropdown with a dom node reference', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown, { reference: fixtureEl }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropdown with a jquery object reference', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown, { reference: { 0: fixtureEl, jquery: 'jQuery' } }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) it('should toggle a dropdown with a valid virtual element reference', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const virtualElement = { nodeType: 1, getBoundingClientRect() { return { width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 } } } expect(() => new Dropdown(btnDropdown, { reference: {} })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') expect(() => new Dropdown(btnDropdown, { reference: { getBoundingClientRect: 'not-a-function' } })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') // use onFirstUpdate as Poppers internal update is executed async const dropdown = new Dropdown(btnDropdown, { reference: virtualElement, popperConfig: { onFirstUpdate() { expect(spy).toHaveBeenCalled() expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() } } }) const spy = spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() dropdown.toggle() }) }) it('should not toggle a dropdown if the element is disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) it('should not toggle a dropdown if the element contains .disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) it('should not toggle a dropdown if the menu is shown', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) it('should not toggle a dropdown if show event is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('show.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) }) describe('show', () => { it('should show a dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') resolve() }) dropdown.show() }) }) it('should not show a dropdown if the element is disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) it('should not show a dropdown if the element contains .disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) it('should not show a dropdown if the menu is shown', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) it('should not show a dropdown if show event is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('show.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) }) describe('hide', () => { it('should hide a dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdownMenu).not.toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') resolve() }) dropdown.hide() }) }) it('should hide a dropdown and destroy popper', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { spyOn(dropdown._popper, 'destroy') dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdown._popper.destroy).toHaveBeenCalled() resolve() }) dropdown.show() }) }) it('should not hide a dropdown if the element is disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }, 10) }) }) it('should not hide a dropdown if the element contains .disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }, 10) }) }) it('should not hide a dropdown if the menu is not shown', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) it('should not hide a dropdown if hide event is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('hide.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }) }) }) it('should remove event listener on touch-enabled device that was added in show method', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const defaultValueOnTouchStart = document.documentElement.ontouchstart const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) document.documentElement.ontouchstart = noop const spy = spyOn(EventHandler, 'off') btnDropdown.addEventListener('shown.bs.dropdown', () => { dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown).not.toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(spy).toHaveBeenCalled() document.documentElement.ontouchstart = defaultValueOnTouchStart resolve() }) dropdown.show() }) }) }) describe('dispose', () => { it('should dispose dropdown', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) expect(dropdown._popper).toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() const spy = spyOn(EventHandler, 'off') dropdown.dispose() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() expect(spy).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) }) it('should dispose dropdown with Popper', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) dropdown.toggle() expect(dropdown._popper).not.toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() dropdown.dispose() expect(dropdown._popper).toBeNull() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() }) }) describe('update', () => { it('should call Popper and detect navbar on update', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) dropdown.toggle() expect(dropdown._popper).not.toBeNull() const spyUpdate = spyOn(dropdown._popper, 'update') const spyDetect = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(spyUpdate).toHaveBeenCalled() expect(spyDetect).toHaveBeenCalled() }) it('should just detect navbar on update', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) const spy = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(dropdown._popper).toBeNull() expect(spy).toHaveBeenCalled() }) }) describe('data-api', () => { it('should show and hide a dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') let showEventTriggered = false let hideEventTriggered = false btnDropdown.addEventListener('show.bs.dropdown', () => { showEventTriggered = true }) btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') expect(showEventTriggered).toBeTrue() expect(event.relatedTarget).toEqual(btnDropdown) document.body.click() })) btnDropdown.addEventListener('hide.bs.dropdown', () => { hideEventTriggered = true }) btnDropdown.addEventListener('hidden.bs.dropdown', event => { expect(btnDropdown).not.toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(hideEventTriggered).toBeTrue() expect(event.relatedTarget).toEqual(btnDropdown) resolve() }) btnDropdown.click() }) }) it('should not use "static" Popper in navbar', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(dropdown._popper).not.toBeNull() expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') resolve() }) dropdown.show() }) }) it('should not collapse the dropdown when clicking a select option nested in the dropdown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) const hideSpy = spyOn(dropdown, '_completeHide') btnDropdown.addEventListener('shown.bs.dropdown', () => { const clickEvent = new MouseEvent('click', { bubbles: true }) dropdownMenu.querySelector('option').dispatchEvent(clickEvent) }) dropdownMenu.addEventListener('click', event => { expect(event.target.tagName).toMatch(/select|option/i) Dropdown.clearMenus(event) setTimeout(() => { expect(hideSpy).not.toHaveBeenCalled() resolve() }, 10) }) dropdown.show() }) }) it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() resolve() }) dropdown.show() }) }) it('should not use Popper if display set to static', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') btnDropdown.addEventListener('shown.bs.dropdown', () => { // Popper adds this attribute when we use it expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() resolve() }) btnDropdown.click() }) }) it('should manage bs attribute `data-bs-popper`="static" when display set to static', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() resolve() }) dropdown.show() }) }) it('should remove "show" class if tabbing outside of menu', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') const keyup = createEvent('keyup') keyup.key = 'Tab' document.dispatchEvent(keyup) }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown).not.toHaveClass('show') resolve() }) btnDropdown.click() }) }) it('should remove "show" class if body is clicked, with multiple dropdowns', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', ' ', ' ', ' ', '
' ].join('') const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') expect(triggerDropdownList).toHaveSize(2) const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdownFirst).toHaveClass('show') expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) document.body.click() }) triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) triggerDropdownLast.click() }) triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdownLast).toHaveClass('show') expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) document.body.click() }) triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) resolve() }) triggerDropdownFirst.click() }) }) it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', ' ', ' ', ' ', '
' ].join('') const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') expect(triggerDropdownList).toHaveSize(2) const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdownFirst).toHaveClass('show') expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) const keyup = createEvent('keyup') keyup.key = 'Tab' document.dispatchEvent(keyup) }) triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) triggerDropdownLast.click() }) triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdownLast).toHaveClass('show') expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) const keyup = createEvent('keyup') keyup.key = 'Tab' document.dispatchEvent(keyup) }) triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) resolve() }) triggerDropdownFirst.click() }) }) it('should be able to identify clicked dropdown, even with multiple dropdowns in the same tag', () => { fixtureEl.innerHTML = [ '' ].join('') const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') const dropdownMenu1 = fixtureEl.querySelector('#menu1') const dropdownMenu2 = fixtureEl.querySelector('#menu2') const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle1.click() expect(spy).toHaveBeenCalledWith(dropdownToggle1) dropdownToggle2.click() expect(spy).toHaveBeenCalledWith(dropdownToggle2) dropdownMenu1.click() expect(spy).toHaveBeenCalledWith(dropdownToggle1) dropdownMenu2.click() expect(spy).toHaveBeenCalledWith(dropdownToggle2) }) it('should be able to show the proper menu, even with multiple dropdowns in the same tag', () => { fixtureEl.innerHTML = [ '' ].join('') const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') const dropdownMenu1 = fixtureEl.querySelector('#menu1') const dropdownMenu2 = fixtureEl.querySelector('#menu2') dropdownToggle1.click() expect(dropdownMenu1).toHaveClass('show') expect(dropdownMenu2).not.toHaveClass('show') dropdownToggle2.click() expect(dropdownMenu1).not.toHaveClass('show') expect(dropdownMenu2).toHaveClass('show') }) it('should fire hide and hidden event without a clickEvent if event type is not click', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') triggerDropdown.addEventListener('hide.bs.dropdown', event => { expect(event.clickEvent).toBeUndefined() }) triggerDropdown.addEventListener('hidden.bs.dropdown', event => { expect(event.clickEvent).toBeUndefined() resolve() }) triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'Escape' triggerDropdown.dispatchEvent(keydown) }) triggerDropdown.click() }) }) it('should bubble up the events to the parent elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownParent = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(triggerDropdown) const showFunction = jasmine.createSpy('showFunction') dropdownParent.addEventListener('show.bs.dropdown', showFunction) const shownFunction = jasmine.createSpy('shownFunction') dropdownParent.addEventListener('shown.bs.dropdown', () => { shownFunction() dropdown.hide() }) const hideFunction = jasmine.createSpy('hideFunction') dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) dropdownParent.addEventListener('hidden.bs.dropdown', () => { expect(showFunction).toHaveBeenCalled() expect(shownFunction).toHaveBeenCalled() expect(hideFunction).toHaveBeenCalled() resolve() }) dropdown.show() }) }) it('should ignore keyboard events within s and ', ' ', '' ].join('') 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') keydown.key = 'ArrowUp' input.dispatchEvent(keydown) expect(document.activeElement).toEqual(input, 'input still focused') textarea.focus() textarea.dispatchEvent(keydown) expect(document.activeElement).toEqual(textarea, 'textarea still focused') resolve() }) triggerDropdown.click() }) }) it('should skip disabled element when using keyboard navigation', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowDown' triggerDropdown.dispatchEvent(keydown) triggerDropdown.dispatchEvent(keydown) expect(document.activeElement).not.toHaveClass('disabled') expect(document.activeElement.hasAttribute('disabled')).toBeFalse() resolve() }) triggerDropdown.click() }) }) it('should skip hidden element when using keyboard navigation', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '' ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowDown' triggerDropdown.dispatchEvent(keydown) expect(document.activeElement).not.toHaveClass('d-none') expect(document.activeElement.style.display).not.toEqual('none') expect(document.activeElement.style.visibility).not.toEqual('hidden') resolve() }) triggerDropdown.click() }) }) 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') triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydownArrowDown = createEvent('keydown') keydownArrowDown.key = 'ArrowDown' triggerDropdown.dispatchEvent(keydownArrowDown) expect(document.activeElement).toEqual(item1, 'item1 is focused') document.activeElement.dispatchEvent(keydownArrowDown) expect(document.activeElement).toEqual(item2, 'item2 is focused') const keydownArrowUp = createEvent('keydown') keydownArrowUp.key = 'ArrowUp' document.activeElement.dispatchEvent(keydownArrowUp) expect(document.activeElement).toEqual(item1, 'item1 is focused') resolve() }) triggerDropdown.click() }) }) 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') 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) }) }) 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') 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) }) }) 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') input.addEventListener('click', () => { expect(triggerDropdown).toHaveClass('show') resolve() }) triggerDropdown.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdown).toHaveClass('show') input.dispatchEvent(createEvent('click')) }) triggerDropdown.click() }) }) 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') textarea.addEventListener('click', () => { expect(triggerDropdown).toHaveClass('show') resolve() }) triggerDropdown.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdown).toHaveClass('show') textarea.dispatchEvent(createEvent('click')) }) triggerDropdown.click() }) }) 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') triggerDropdown.addEventListener('hidden.bs.dropdown', () => { expect().nothing() resolve() }) triggerDropdown.addEventListener('shown.bs.dropdown', () => { input.dispatchEvent(createEvent('click', { bubbles: true })) }) 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`) } const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' triggerDropdown.addEventListener('shown.bs.dropdown', () => { // Key Space test('Space', input) test('Space', textarea) // Key ArrowUp test('ArrowUp', input) test('ArrowUp', textarea) // Key ArrowDown test('ArrowDown', input) test('ArrowDown', textarea) // Key Escape input.focus() input.dispatchEvent(keydownEscape) expect(triggerDropdown).not.toHaveClass('show') resolve() }) triggerDropdown.click() }) }) it('should not open dropdown if escape key was pressed on the toggle', () => { 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 spy = spyOn(dropdown, 'toggle') // Key escape button.focus() // Key escape const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' button.dispatchEvent(keydownEscape) 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() }) const keydownEscape = createEvent('keydown', { bubbles: true }) keydownEscape.key = 'Escape' const keyupEscape = createEvent('keyup', { bubbles: true }) keyupEscape.key = 'Escape' toggle.focus() toggle.dispatchEvent(keydownEscape) toggle.dispatchEvent(keyupEscape) }) }) it('should not propagate escape key events if dropdown is open', () => { 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).not.toHaveBeenCalled() resolve() }) const keydownEscape = createEvent('keydown', { bubbles: true }) keydownEscape.key = 'Escape' const keyupEscape = createEvent('keyup', { bubbles: true }) keyupEscape.key = 'Escape' toggle.click() toggle.dispatchEvent(keydownEscape) toggle.dispatchEvent(keyupEscape) }) }) it('should close dropdown using `escape` button, and return focus to its trigger', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') toggle.addEventListener('shown.bs.dropdown', () => { const keydownEvent = createEvent('keydown', { bubbles: true }) keydownEvent.key = 'ArrowDown' toggle.dispatchEvent(keydownEvent) keydownEvent.key = 'Escape' toggle.dispatchEvent(keydownEvent) }) toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { expect(document.activeElement).toEqual(toggle) resolve() })) toggle.click() }) }) 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 dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const expectDropdownToBeOpened = () => setTimeout(() => { expect(dropdownToggle).toHaveClass('show') dropdownMenu.click() }, 150) dropdownToggle.addEventListener('shown.bs.dropdown', () => { document.documentElement.click() expectDropdownToBeOpened() }) dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { expect(dropdownToggle).not.toHaveClass('show') resolve() })) dropdownToggle.click() }) }) 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 expectDropdownToBeOpened = () => setTimeout(() => { expect(dropdownToggle).toHaveClass('show') document.documentElement.click() }, 150) dropdownToggle.addEventListener('shown.bs.dropdown', () => { dropdownMenu.click() expectDropdownToBeOpened() }) dropdownToggle.addEventListener('hidden.bs.dropdown', () => { expect(dropdownToggle).not.toHaveClass('show') resolve() }) 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"`', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { expect(dropdownToggle).toHaveClass('show') if (shouldTriggerClick) { document.documentElement.click() } else { resolve() } expectDropdownToBeOpened(false) }, 150) dropdownToggle.addEventListener('shown.bs.dropdown', () => { dropdownMenu.click() expectDropdownToBeOpened() }) dropdownToggle.click() }) }) 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 spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle.click() expect(spy).toHaveBeenCalledWith(dropdownToggle) dropdownMenu.click() expect(spy).toHaveBeenCalledWith(dropdownToggle) }) }) describe('jQueryInterface', () => { it('should create a dropdown', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.dropdown = Dropdown.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.dropdown.call(jQueryMock) expect(Dropdown.getInstance(div)).not.toBeNull() }) it('should not re create a dropdown', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const dropdown = new Dropdown(div) jQueryMock.fn.dropdown = Dropdown.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.dropdown.call(jQueryMock) expect(Dropdown.getInstance(div)).toEqual(dropdown) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.dropdown = Dropdown.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.dropdown.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return dropdown instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const dropdown = new Dropdown(div) expect(Dropdown.getInstance(div)).toEqual(dropdown) expect(Dropdown.getInstance(div)).toBeInstanceOf(Dropdown) }) it('should return null when there is no dropdown instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Dropdown.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return dropdown instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const dropdown = new Dropdown(div) expect(Dropdown.getOrCreateInstance(div)).toEqual(dropdown) expect(Dropdown.getInstance(div)).toEqual(Dropdown.getOrCreateInstance(div, {})) expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) }) it('should return new instance when there is no dropdown instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Dropdown.getInstance(div)).toBeNull() expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) }) it('should return new instance when there is no dropdown instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Dropdown.getInstance(div)).toBeNull() const dropdown = Dropdown.getOrCreateInstance(div, { display: 'dynamic' }) expect(dropdown).toBeInstanceOf(Dropdown) expect(dropdown._config.display).toEqual('dynamic') }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const dropdown = new Dropdown(div, { display: 'dynamic' }) expect(Dropdown.getInstance(div)).toEqual(dropdown) const dropdown2 = Dropdown.getOrCreateInstance(div, { display: 'static' }) expect(dropdown).toBeInstanceOf(Dropdown) expect(dropdown2).toEqual(dropdown) expect(dropdown2._config.display).toEqual('dynamic') }) }) 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 keydown = createEvent('keydown') keydown.key = 'ArrowDown' const keyup = createEvent('keyup') keyup.key = 'ArrowUp' const handleArrowDown = () => { expect(triggerDropdown).toHaveClass('show') expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') setTimeout(() => { dropdown.hide() keydown.key = 'ArrowUp' triggerDropdown.dispatchEvent(keyup) }, 20) } 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', () => { fixtureEl.innerHTML = [ '' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const clickListener = jasmine.createSpy('clickListener') const delegatedClickListener = jasmine.createSpy('delegatedClickListener') btnDropdown.addEventListener('click', clickListener) document.addEventListener('click', delegatedClickListener) btnDropdown.click() expect(clickListener).toHaveBeenCalled() expect(delegatedClickListener).toHaveBeenCalled() }) 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') btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() })) childElement.click() }) }) }) ================================================ FILE: js/tests/unit/jquery.spec.js ================================================ /* eslint-env jquery */ 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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) it('should add all plugins in jQuery', () => { expect(Alert.jQueryInterface).toEqual(jQuery.fn.alert) expect(Button.jQueryInterface).toEqual(jQuery.fn.button) expect(Carousel.jQueryInterface).toEqual(jQuery.fn.carousel) 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) expect(Toast.jQueryInterface).toEqual(jQuery.fn.toast) expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip) }) 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') }) }) }) ================================================ FILE: js/tests/unit/modal.spec.js ================================================ 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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() clearBodyAndDocument() document.body.classList.remove('modal-open') for (const backdrop of document.querySelectorAll('.modal-backdrop')) { backdrop.remove() } }) beforeEach(() => { clearBodyAndDocument() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Modal.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Modal.Default).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Modal.DATA_KEY).toEqual('bs.modal') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modalBySelector = new Modal('.modal') const modalByElement = new Modal(modalEl) expect(modalBySelector._element).toEqual(modalEl) expect(modalByElement._element).toEqual(modalEl) }) }) describe('toggle', () => { 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() }) modalEl.addEventListener('hidden.bs.modal', () => { expect(spyReset).toHaveBeenCalled() resolve() }) modal.toggle() }) }) }) describe('show', () => { it('should show a modal', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) 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() resolve() }) modal.show() }) }) 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 }) 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() resolve() }) modal.show() }) }) 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 = '' const modal = new Modal(modalEl) modalEl.addEventListener('shown.bs.modal', () => { const dynamicModal = document.getElementById(id) expect(dynamicModal).not.toBeNull() dynamicModal.remove() resolve() }) modal.show() }) }) it('should do nothing if a modal is shown', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const spy = spyOn(EventHandler, 'trigger') modal._isShown = true modal.show() expect(spy).not.toHaveBeenCalled() }) it('should do nothing if a modal is transitioning', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const spy = spyOn(EventHandler, 'trigger') modal._isTransitioning = true modal.show() expect(spy).not.toHaveBeenCalled() }) 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) modalEl.addEventListener('show.bs.modal', event => { event.preventDefault() const expectedDone = () => { expect().nothing() resolve() } setTimeout(expectedDone, 10) }) modalEl.addEventListener('shown.bs.modal', () => { reject(new Error('shown event triggered')) }) modal.show() }) }) 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) let prevented = false modalEl.addEventListener('show.bs.modal', event => { if (!prevented) { event.preventDefault() prevented = true setTimeout(() => { modal.show() }) } }) modalEl.addEventListener('shown.bs.modal', () => { expect(prevented).toBeTrue() expect(modal._isAnimated()).toBeTrue() resolve() }) modal.show() }) }) it('should set is transitioning if fade class is present', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) modalEl.addEventListener('show.bs.modal', () => { setTimeout(() => { expect(modal._isTransitioning).toBeTrue() }) }) modalEl.addEventListener('shown.bs.modal', () => { expect(modal._isTransitioning).toBeFalse() resolve() }) modal.show() }) }) 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('hidden.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) 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 spy = spyOn(modal, 'hide').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { btnClose.click() }) modalEl.addEventListener('hidden.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) 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) modalEl.addEventListener('shown.bs.modal', () => { expect(modalEl.scrollTop).toEqual(0) resolve() }) modal.show() }) }) 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() }) modal.show() }) }) 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 spy = spyOn(modal._focustrap, 'activate').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { expect(spy).not.toHaveBeenCalled() resolve() }) modal.show() }) }) 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 spy = spyOn(modal, 'hide').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' modalEl.dispatchEvent(keydownEscape) }) modalEl.addEventListener('hidden.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) 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 spy = spyOn(modal, 'hide') const expectDone = () => { expect(spy).not.toHaveBeenCalled() resolve() } modalEl.addEventListener('shown.bs.modal', () => { const keydownTab = createEvent('keydown') keydownTab.key = 'Tab' modalEl.dispatchEvent(keydownTab) setTimeout(expectDone, 30) }) modal.show() }) }) it('should adjust dialog on resize', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const spy = spyOn(modal, '_adjustDialog').and.callThrough() const expectDone = () => { expect(spy).toHaveBeenCalled() resolve() } modalEl.addEventListener('shown.bs.modal', () => { const resizeEvent = createEvent('resize') window.dispatchEvent(resizeEvent) setTimeout(expectDone, 10) }) modal.show() }) }) 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) const shownCallback = () => { setTimeout(() => { expect(modal._isShown).toEqual(true) resolve() }, 10) } modalEl.addEventListener('shown.bs.modal', () => { fixtureEl.querySelector('.modal-dialog').click() fixtureEl.querySelector('.modal-content').click() shownCallback() }) modalEl.addEventListener('hidden.bs.modal', () => { reject(new Error('Should not hide a modal')) }) modal.show() }) }) 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: false }) const shownCallback = () => { setTimeout(() => { expect(modal._isShown).toBeTrue() resolve() }, 10) } modalEl.addEventListener('shown.bs.modal', () => { modalEl.click() shownCallback() }) modalEl.addEventListener('hidden.bs.modal', () => { reject(new Error('Should not hide a modal')) }) modal.show() }) }) 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' }) const shownCallback = () => { setTimeout(() => { expect(modal._isShown).toBeTrue() resolve() }, 10) } modalEl.addEventListener('shown.bs.modal', () => { modalEl.click() shownCallback() }) modalEl.addEventListener('hidden.bs.modal', () => { reject(new Error('Should not hide a modal')) }) 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 }) const shownCallback = () => { setTimeout(() => { expect(modal._isShown).toBeFalse() resolve() }, 10) } modalEl.addEventListener('shown.bs.modal', () => { const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' modalEl.dispatchEvent(keydownEscape) shownCallback() }) modal.show() }) }) it('should not close modal when escape key is pressed with keyboard = false', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl, { keyboard: false }) const shownCallback = () => { setTimeout(() => { expect(modal._isShown).toBeTrue() resolve() }, 10) } modalEl.addEventListener('shown.bs.modal', () => { const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' modalEl.dispatchEvent(keydownEscape) shownCallback() }) modalEl.addEventListener('hidden.bs.modal', () => { reject(new Error('Should not hide a modal')) }) modal.show() }) }) it('should not overflow when clicking outside of modal-content if backdrop = static', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl, { backdrop: 'static' }) modalEl.addEventListener('shown.bs.modal', () => { modalEl.click() setTimeout(() => { expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) resolve() }, 20) }) modal.show() }) }) it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl, { backdrop: 'static' }) modalEl.addEventListener('shown.bs.modal', () => { const spy = spyOn(modal, '_queueCallback').and.callThrough() const mouseDown = createEvent('mousedown') modalEl.dispatchEvent(mouseDown) modalEl.click() modalEl.dispatchEvent(mouseDown) modalEl.click() setTimeout(() => { expect(spy).toHaveBeenCalledTimes(1) resolve() }, 20) }) modal.show() }) }) it('should trap focus', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const spy = spyOn(modal._focustrap, 'activate').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) }) describe('hide', () => { it('should hide a modal', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' 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('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(backdropSpy).toHaveBeenCalled() resolve() }) modal.show() }) }) it('should close modal when clicking outside of modal-content', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const dialogEl = modalEl.querySelector('.modal-dialog') const modal = new Modal(modalEl) const spy = spyOn(modal, 'hide') modalEl.addEventListener('shown.bs.modal', () => { const mouseDown = createEvent('mousedown') dialogEl.dispatchEvent(mouseDown) modalEl.click() expect(spy).not.toHaveBeenCalled() modalEl.dispatchEvent(mouseDown) modalEl.click() expect(spy).toHaveBeenCalled() resolve() }) 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', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) modal.hide() expect().nothing() }) it('should do nothing is the modal is transitioning', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) modal._isTransitioning = true modal.hide() expect().nothing() }) 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) modalEl.addEventListener('shown.bs.modal', () => { modal.hide() }) const hideCallback = () => { setTimeout(() => { expect(modal._isShown).toBeTrue() resolve() }, 10) } modalEl.addEventListener('hide.bs.modal', event => { event.preventDefault() hideCallback() }) modalEl.addEventListener('hidden.bs.modal', () => { reject(new Error('should not trigger hidden')) }) modal.show() }) }) it('should release focus trap', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' 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('hidden.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) }) describe('dispose', () => { it('should dispose a modal', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const focustrap = modal._focustrap const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Modal.getInstance(modalEl)).toEqual(modal) const spyOff = spyOn(EventHandler, 'off') modal.dispose() expect(Modal.getInstance(modalEl)).toBeNull() expect(spyOff).toHaveBeenCalledTimes(3) expect(spyDeactivate).toHaveBeenCalled() }) }) describe('handleUpdate', () => { it('should call adjust dialog', () => { fixtureEl.innerHTML = '' const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const spy = spyOn(modal, '_adjustDialog') modal.handleUpdate() expect(spy).toHaveBeenCalled() }) }) describe('data-api', () => { 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) }) 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() }) trigger.click() }) }) 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 spy = spyOn(modal, 'show').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) trigger.click() }) }) 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() }) trigger.click() }) }) 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 spy = spyOn(trigger, 'focus') modalEl.addEventListener('shown.bs.modal', () => { const modal = Modal.getInstance(modalEl) modal.hide() }) const hideListener = () => { setTimeout(() => { expect(spy).toHaveBeenCalled() resolve() }, 20) } modalEl.addEventListener('hidden.bs.modal', () => { hideListener() }) 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 ', () => { 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 spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { btnClose.click() }) modalEl.addEventListener('hidden.bs.modal', () => { expect(spy).not.toHaveBeenCalled() resolve() }) modal.show() }) }) 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 spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() modalEl.addEventListener('shown.bs.modal', () => { btnClose.click() }) modalEl.addEventListener('hidden.bs.modal', () => { expect(spy).toHaveBeenCalled() resolve() }) modal.show() }) }) it('should not focus the trigger if the modal is not visible', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '' ].join('') const modalEl = fixtureEl.querySelector('.modal') const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') const spy = spyOn(trigger, 'focus') modalEl.addEventListener('shown.bs.modal', () => { const modal = Modal.getInstance(modalEl) modal.hide() }) const hideListener = () => { setTimeout(() => { expect(spy).not.toHaveBeenCalled() resolve() }, 20) } modalEl.addEventListener('hidden.bs.modal', () => { hideListener() }) trigger.click() }) }) it('should not focus the trigger if the modal is not shown', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '' ].join('') const modalEl = fixtureEl.querySelector('.modal') const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') const spy = spyOn(trigger, 'focus') const showListener = () => { setTimeout(() => { expect(spy).not.toHaveBeenCalled() resolve() }, 10) } modalEl.addEventListener('show.bs.modal', event => { event.preventDefault() showListener() }) trigger.click() }) }) 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() }) }) }) describe('jQueryInterface', () => { it('should create a modal', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.modal.call(jQueryMock) expect(Modal.getInstance(div)).not.toBeNull() }) it('should create a modal with given config', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.modal.call(jQueryMock, { 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).toBeFalse() }) it('should not re create a modal', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') const modal = new Modal(div) jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.modal.call(jQueryMock) expect(Modal.getInstance(div)).toEqual(modal) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.modal.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should call show method', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') const modal = new Modal(div) jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] const spy = spyOn(modal, 'show') jQueryMock.fn.modal.call(jQueryMock, 'show') expect(spy).toHaveBeenCalled() }) it('should not call show method', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] const spy = spyOn(Modal.prototype, 'show') jQueryMock.fn.modal.call(jQueryMock) expect(spy).not.toHaveBeenCalled() }) }) describe('getInstance', () => { it('should return modal instance', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') const modal = new Modal(div) expect(Modal.getInstance(div)).toEqual(modal) expect(Modal.getInstance(div)).toBeInstanceOf(Modal) }) it('should return null when there is no modal instance', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') expect(Modal.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return modal instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const modal = new Modal(div) expect(Modal.getOrCreateInstance(div)).toEqual(modal) expect(Modal.getInstance(div)).toEqual(Modal.getOrCreateInstance(div, {})) expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal) }) it('should return new instance when there is no modal instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Modal.getInstance(div)).toBeNull() expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal) }) it('should return new instance when there is no modal instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Modal.getInstance(div)).toBeNull() const modal = Modal.getOrCreateInstance(div, { backdrop: true }) expect(modal).toBeInstanceOf(Modal) expect(modal._config.backdrop).toBeTrue() }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const modal = new Modal(div, { backdrop: true }) expect(Modal.getInstance(div)).toEqual(modal) const modal2 = Modal.getOrCreateInstance(div, { backdrop: false }) expect(modal).toBeInstanceOf(Modal) expect(modal2).toEqual(modal) expect(modal2._config.backdrop).toBeTrue() }) }) }) ================================================ FILE: js/tests/unit/offcanvas.spec.js ================================================ 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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() document.body.classList.remove('offcanvas-open') clearBodyAndDocument() }) beforeEach(() => { clearBodyAndDocument() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Offcanvas.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Offcanvas.Default).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Offcanvas.DATA_KEY).toEqual('bs.offcanvas') }) }) describe('constructor', () => { it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => { fixtureEl.innerHTML = [ '
', ' Close', '
' ].join('') const offCanvasEl = fixtureEl.querySelector('.offcanvas') const closeEl = fixtureEl.querySelector('a') const offCanvas = new Offcanvas(offCanvasEl) const spy = spyOn(offCanvas, 'hide') closeEl.click() expect(offCanvas._config.keyboard).toBeTrue() expect(spy).toHaveBeenCalled() }) it('should hide if esc is pressed', () => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) const keyDownEsc = createEvent('keydown') keyDownEsc.key = 'Escape' 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(spy).toHaveBeenCalled() }) it('should not hide if esc is not pressed', () => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) const keydownTab = createEvent('keydown') keydownTab.key = 'Tab' const spy = spyOn(offCanvas, 'hide') offCanvasEl.dispatchEvent(keydownTab) expect(spy).not.toHaveBeenCalled() }) it('should not hide if esc is pressed but with keyboard = false', () => { 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 spy = spyOn(offCanvas, 'hide') const hidePreventedSpy = jasmine.createSpy('hidePrevented') offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) offCanvasEl.addEventListener('shown.bs.offcanvas', () => { expect(offCanvas._config.keyboard).toBeFalse() offCanvasEl.dispatchEvent(keyDownEsc) expect(hidePreventedSpy).toHaveBeenCalled() expect(spy).not.toHaveBeenCalled() resolve() }) 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 = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) 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 = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) 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 = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true, keyboard: true, scroll: 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', () => { 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() }) }) 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() }) }) it('should hide a shown element if user click on backdrop', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) 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() }) }) it('should not trap focus if scroll is allowed', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl, { scroll: true, backdrop: false }) const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() offCanvasEl.addEventListener('shown.bs.offcanvas', () => { expect(spy).not.toHaveBeenCalled() resolve() }) offCanvas.show() }) }) 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, backdrop: true }) const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() offCanvasEl.addEventListener('shown.bs.offcanvas', () => { expect(spy).toHaveBeenCalled() resolve() }) offCanvas.show() }) }) }) describe('toggle', () => { it('should call show method if show class is not present', () => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) const spy = spyOn(offCanvas, 'show') offCanvas.toggle() expect(spy).toHaveBeenCalled() }) it('should call hide method if show class is present', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) offCanvasEl.addEventListener('shown.bs.offcanvas', () => { expect(offCanvasEl).toHaveClass('show') const spy = spyOn(offCanvas, 'hide') offCanvas.toggle() expect(spy).toHaveBeenCalled() resolve() }) 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 = '
' const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) offCanvas.show() expect(offCanvasEl).toHaveClass('show') const spyShow = spyOn(offCanvas._backdrop, 'show').and.callThrough() const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() offCanvas.show() expect(spyTrigger).not.toHaveBeenCalled() expect(spyShow).not.toHaveBeenCalled() }) it('should show a hidden element', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' 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).toHaveClass('show') expect(spy).toHaveBeenCalled() resolve() }) offCanvas.show() }) }) 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) const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough() const expectEnd = () => { setTimeout(() => { expect(spy).not.toHaveBeenCalled() resolve() }, 10) } 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"', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('div') const spy = spyOn(Offcanvas.prototype, 'show').and.callThrough() offCanvasEl.addEventListener('shown.bs.offcanvas', () => { resolve() }) window.dispatchEvent(createEvent('load')) const instance = Offcanvas.getInstance(offCanvasEl) expect(instance).not.toBeNull() expect(spy).toHaveBeenCalled() }) }) it('should trap focus', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() offCanvasEl.addEventListener('shown.bs.offcanvas', () => { expect(spy).toHaveBeenCalled() resolve() }) 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 = '
' const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough() offCanvas.hide() expect(spyHide).not.toHaveBeenCalled() expect(spyTrigger).not.toHaveBeenCalled() }) it('should hide a shown element', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' 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).not.toHaveClass('show') expect(spy).toHaveBeenCalled() resolve() }) offCanvas.hide() }) }) 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) const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough() offCanvas.show() const expectEnd = () => { setTimeout(() => { expect(spy).not.toHaveBeenCalled() resolve() }, 10) } offCanvasEl.addEventListener('hide.bs.offcanvas', event => { event.preventDefault() expectEnd() }) offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { reject(new Error('should not fire hidden event')) }) offCanvas.hide() }) }) it('should release focus trap', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' 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(spy).toHaveBeenCalled() resolve() }) offCanvas.hide() }) }) }) describe('dispose', () => { it('should dispose an offcanvas', () => { fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) const backdrop = offCanvas._backdrop const spyDispose = spyOn(backdrop, 'dispose').and.callThrough() const focustrap = offCanvas._focustrap const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas) offCanvas.dispose() expect(spyDispose).toHaveBeenCalled() expect(offCanvas._backdrop).toBeNull() expect(spyDeactivate).toHaveBeenCalled() expect(offCanvas._focustrap).toBeNull() expect(Offcanvas.getInstance(offCanvasEl)).toBeNull() }) }) describe('data-api', () => { 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() }) }) it('should not call toggle on disabled elements', () => { fixtureEl.innerHTML = [ '', '
' ].join('') const target = fixtureEl.querySelector('a') const spy = spyOn(Offcanvas.prototype, 'toggle') target.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() }) }) 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() }) }) 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() }) }) }) describe('jQueryInterface', () => { it('should create an offcanvas', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock) expect(Offcanvas.getInstance(div)).not.toBeNull() }) it('should not re create an offcanvas', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(div) jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock) expect(Offcanvas.getInstance(div)).toEqual(offCanvas) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.offcanvas.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should throw error on protected method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = '_getConfig' jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.offcanvas.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should throw error if method "constructor" is being called', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'constructor' jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.offcanvas.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should call offcanvas method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const spy = spyOn(Offcanvas.prototype, 'show') jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock, 'show') expect(spy).toHaveBeenCalled() }) it('should create a offcanvas with given config', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock, { scroll: true }) const offcanvas = Offcanvas.getInstance(div) expect(offcanvas).not.toBeNull() expect(offcanvas._config.scroll).toBeTrue() }) }) describe('getInstance', () => { it('should return offcanvas instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(div) expect(Offcanvas.getInstance(div)).toEqual(offCanvas) expect(Offcanvas.getInstance(div)).toBeInstanceOf(Offcanvas) }) it('should return null when there is no offcanvas instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Offcanvas.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return offcanvas instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const offcanvas = new Offcanvas(div) expect(Offcanvas.getOrCreateInstance(div)).toEqual(offcanvas) expect(Offcanvas.getInstance(div)).toEqual(Offcanvas.getOrCreateInstance(div, {})) expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas) }) it('should return new instance when there is no Offcanvas instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Offcanvas.getInstance(div)).toBeNull() expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas) }) it('should return new instance when there is no offcanvas instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Offcanvas.getInstance(div)).toBeNull() const offcanvas = Offcanvas.getOrCreateInstance(div, { scroll: true }) expect(offcanvas).toBeInstanceOf(Offcanvas) expect(offcanvas._config.scroll).toBeTrue() }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const offcanvas = new Offcanvas(div, { scroll: true }) expect(Offcanvas.getInstance(div)).toEqual(offcanvas) const offcanvas2 = Offcanvas.getOrCreateInstance(div, { scroll: false }) expect(offcanvas).toBeInstanceOf(Offcanvas) expect(offcanvas2).toEqual(offcanvas) expect(offcanvas2._config.scroll).toBeTrue() }) }) }) ================================================ FILE: js/tests/unit/popover.spec.js ================================================ 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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() const popoverList = document.querySelectorAll('.popover') for (const popoverEl of popoverList) { popoverEl.remove() } }) describe('VERSION', () => { it('should return plugin version', () => { expect(Popover.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Popover.Default).toEqual(jasmine.any(Object)) }) }) describe('NAME', () => { it('should return plugin name', () => { expect(Popover.NAME).toEqual(jasmine.any(String)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Popover.DATA_KEY).toEqual('bs.popover') }) }) describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(Popover.EVENT_KEY).toEqual('.bs.popover') }) }) describe('DefaultType', () => { it('should return plugin default type', () => { expect(Popover.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('show', () => { 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() }) popover.show() }) }) it('should show a popover', () => { 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() resolve() }) popover.show() }) }) 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, { title: () => 'Bootstrap', content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' }) popoverEl.addEventListener('shown.bs.popover', () => { const popoverDisplayed = document.querySelector('.popover') 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() }) }) 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() }) popover.show() }) }) 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() }) 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', () => { 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' }) 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' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) 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('"setContent" should keep the initial template', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) 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() }) 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' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) popoverEl.addEventListener('shown.bs.popover', () => { const tip = document.querySelector('.popover') expect(tip).not.toBeNull() expect(tip).toHaveClass('custom-class') resolve() }) popover.show() }) }) it('should keep popover open when mouse leaves after click trigger', () => { return new Promise(resolve => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') new Popover(popoverEl) // eslint-disable-line no-new popoverEl.addEventListener('shown.bs.popover', () => { popoverEl.dispatchEvent(createEvent('mouseout')) 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', () => { return new Promise(resolve => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) popoverEl.addEventListener('shown.bs.popover', () => { popover.hide() }) popoverEl.addEventListener('hidden.bs.popover', () => { expect(document.querySelector('.popover')).toBeNull() resolve() }) popover.show() }) }) }) describe('jQueryInterface', () => { it('should create a popover', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] jQueryMock.fn.popover.call(jQueryMock) expect(Popover.getInstance(popoverEl)).not.toBeNull() }) it('should create a popover with a config object', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] jQueryMock.fn.popover.call(jQueryMock, { content: 'Popover content' }) expect(Popover.getInstance(popoverEl)).not.toBeNull() }) it('should not re create a popover', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] jQueryMock.fn.popover.call(jQueryMock) expect(Popover.getInstance(popoverEl)).toEqual(popover) }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const action = 'undefinedMethod' jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] expect(() => { jQueryMock.fn.popover.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should should call show method', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] const spy = spyOn(popover, 'show') jQueryMock.fn.popover.call(jQueryMock, 'show') expect(spy).toHaveBeenCalled() }) }) describe('getInstance', () => { it('should return popover instance', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) expect(Popover.getInstance(popoverEl)).toEqual(popover) expect(Popover.getInstance(popoverEl)).toBeInstanceOf(Popover) }) it('should return null when there is no popover instance', () => { fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') expect(Popover.getInstance(popoverEl)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return popover instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const popover = new Popover(div) expect(Popover.getOrCreateInstance(div)).toEqual(popover) expect(Popover.getInstance(div)).toEqual(Popover.getOrCreateInstance(div, {})) expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover) }) it('should return new instance when there is no popover instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Popover.getInstance(div)).toBeNull() expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover) }) it('should return new instance when there is no popover instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Popover.getInstance(div)).toBeNull() const popover = Popover.getOrCreateInstance(div, { placement: 'top' }) expect(popover).toBeInstanceOf(Popover) expect(popover._config.placement).toEqual('top') }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const popover = new Popover(div, { placement: 'top' }) expect(Popover.getInstance(div)).toEqual(popover) const popover2 = Popover.getOrCreateInstance(div, { placement: 'bottom' }) expect(popover).toBeInstanceOf(Popover) expect(popover2).toEqual(popover) expect(popover2._config.placement).toEqual('top') }) }) }) ================================================ FILE: js/tests/unit/scrollspy.spec.js ================================================ 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 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 = 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() }) setTimeout(() => { // in case we scroll something before the test scrollTo(contentEl, scrollHeight) }, 100) } beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(ScrollSpy.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(ScrollSpy.Default).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(ScrollSpy.DATA_KEY).toEqual('bs.scrollspy') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = getDummyFixture() 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 null, if element is not scrollable', () => { fixtureEl.innerHTML = [ '', '
', '
test
', '
' ].join('') const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { target: '#navigation' }) expect(scrollSpy._observer.root).toBeNull() expect(scrollSpy._rootElement).toBeNull() }) it('should respect threshold option', () => { fixtureEl.innerHTML = [ '', '
', ' ', '
' ].join('') const scrollSpy = new ScrollSpy('#content', { target: '#navigation', threshold: [1] }) expect(scrollSpy._observer.thresholds).toEqual([1]) }) it('should respect threshold option markup', () => { fixtureEl.innerHTML = [ '', '
', ' ', '
' ].join('') const scrollSpy = new ScrollSpy('#content', { target: '#navigation' }) // 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]) } } expectToBeCloseToArray(scrollSpy._observer.thresholds, [0, 0.2, 1]) }) it('should not take count to not visible sections', () => { fixtureEl.innerHTML = [ '', '
', '
test
', ' ', ' ', '
' ].join('') const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { target: '#navigation' }) expect(scrollSpy._observableSections.size).toBe(1) expect(scrollSpy._targetLinks.size).toBe(1) }) it('should not process element without target', () => { fixtureEl.innerHTML = [ '', '
', '
test
', '
test2
', '
' ].join('') const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { target: '#navigation' }) expect(scrollSpy._targetLinks).toHaveSize(2) }) it('should only switch "active" class on current target', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', ' ', '
', '
', '
', '
', '
Overview
', '
Detail
', '
', '
' ].join('') const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') const rootEl = fixtureEl.querySelector('#root') const scrollSpy = new ScrollSpy(scrollSpyEl, { target: 'ss-target' }) const spy = spyOn(scrollSpy, '_process').and.callThrough() onScrollStop(() => { expect(rootEl).toHaveClass('active') expect(spy).toHaveBeenCalled() resolve() }, scrollSpyEl) scrollTo(scrollSpyEl, 350) }) }) it('should not process data if `activeTarget` is same as given target', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') const contentEl = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(contentEl, { offset: 0, target: '.navbar' }) const triggerSpy = spyOn(EventHandler, 'trigger').and.callThrough() scrollSpy._activeTarget = fixtureEl.querySelector('#a-1') testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, cb: reject }) setTimeout(() => { expect(triggerSpy).not.toHaveBeenCalled() resolve() }, 100) }) }) it('should only switch "active" class on current target specified w element', () => { return new Promise(resolve => { 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 spy = spyOn(scrollSpy, '_process').and.callThrough() onScrollStop(() => { expect(rootEl).toHaveClass('active') expect(scrollSpy._activeTarget).toEqual(fixtureEl.querySelector('[href="#detail"]')) expect(spy).toHaveBeenCalled() resolve() }, scrollSpyEl) scrollTo(scrollSpyEl, 350) }) }) it('should add the active class to the correct element', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') const contentEl = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(contentEl, { offset: 0, target: '.navbar' }) testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, cb: resolve }) } }) }) }) it('should add to nav the active class to the correct element (nav markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') const contentEl = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(contentEl, { offset: 0, target: '.navbar' }) testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, cb: resolve }) } }) }) }) it('should add to list-group, the active class to the correct element (list-group markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') const contentEl = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(contentEl, { offset: 0, target: '.navbar' }) testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, cb: resolve }) } }) }) }) it('should clear selection if above the first section', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
', '
', '
text
', '
text
', '
text
', '
', '
' ].join('') const contentEl = fixtureEl.querySelector('#content') const scrollSpy = new ScrollSpy(contentEl, { target: '#navigation', offset: contentEl.offsetTop }) const spy = spyOn(scrollSpy, '_process').and.callThrough() onScrollStop(() => { const active = () => fixtureEl.querySelector('.active') expect(spy).toHaveBeenCalled() expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) expect(active().getAttribute('id')).toEqual('two-link') onScrollStop(() => { expect(active()).toBeNull() resolve() }, contentEl) scrollTo(contentEl, 0) }, contentEl) scrollTo(contentEl, 200) }) }) it('should not clear selection if above the first section and first section is at the top', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
', '
test
', '
test
', '
test
', '
test
', '
' ].join('') const negativeHeight = 0 const startOfSectionTwo = 101 const contentEl = fixtureEl.querySelector('#content') // eslint-disable-next-line no-unused-vars const scrollSpy = new ScrollSpy(contentEl, { target: '#navigation', rootMargin: '0px 0px -50%' }) onScrollStop(() => { const activeId = () => fixtureEl.querySelector('.active').getAttribute('id') expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) expect(activeId()).toEqual('two-link') scrollTo(contentEl, negativeHeight) onScrollStop(() => { expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) expect(activeId()).toEqual('one-link') resolve() }, contentEl) scrollTo(contentEl, 0) }, contentEl) scrollTo(contentEl, startOfSectionTwo) }) }) it('should correctly select navigation element on backward scrolling when each target section height is 100%', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
div 3
', '
div 4
', '
div 5
', '
' ].join('') const contentEl = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(contentEl, { offset: 0, target: '.navbar' }) scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-5', targetSelector: '#div-100-5', contentEl, scrollSpy, cb() { scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-2', targetSelector: '#div-100-2', contentEl, scrollSpy, cb() { scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-3', targetSelector: '#div-100-3', contentEl, scrollSpy, cb() { scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-2', targetSelector: '#div-100-2', contentEl, scrollSpy, cb() { scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-1', targetSelector: '#div-100-1', contentEl, scrollSpy, cb: resolve }) } }) } }) } }) } }) }) }) }) describe('refresh', () => { it('should disconnect existing observer', () => { fixtureEl.innerHTML = getDummyFixture() const el = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(el) const spy = spyOn(scrollSpy._observer, 'disconnect') scrollSpy.refresh() expect(spy).toHaveBeenCalled() }) }) describe('dispose', () => { it('should dispose a scrollspy', () => { fixtureEl.innerHTML = getDummyFixture() const el = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(el) expect(ScrollSpy.getInstance(el)).not.toBeNull() scrollSpy.dispose() expect(ScrollSpy.getInstance(el)).toBeNull() }) }) describe('jQueryInterface', () => { it('should create a scrollspy', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock, { target: '#navBar' }) expect(ScrollSpy.getInstance(div)).not.toBeNull() }) it('should create a scrollspy with given config', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock, { rootMargin: '100px' }) const spy = spyOn(ScrollSpy.prototype, 'constructor') expect(spy).not.toHaveBeenCalledWith(div, { rootMargin: '100px' }) const scrollspy = ScrollSpy.getInstance(div) expect(scrollspy).not.toBeNull() expect(scrollspy._config.rootMargin).toEqual('100px') }) it('should not re create a scrollspy', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock) expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) }) it('should call a scrollspy method', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) const spy = spyOn(scrollSpy, 'refresh') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh') expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const action = 'undefinedMethod' jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.scrollspy.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should throw error on protected method', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const action = '_getConfig' jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.scrollspy.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) it('should throw error if method "constructor" is being called', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const action = 'constructor' jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.scrollspy.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return scrollspy instance', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div, { target: fixtureEl.querySelector('#navBar') }) expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return null if there is no instance', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') expect(ScrollSpy.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return scrollspy instance', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div) expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy) expect(ScrollSpy.getInstance(div)).toEqual(ScrollSpy.getOrCreateInstance(div, {})) expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return new instance when there is no scrollspy instance', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') expect(ScrollSpy.getInstance(div)).toBeNull() expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return new instance when there is no scrollspy instance with given configuration', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') expect(ScrollSpy.getInstance(div)).toBeNull() const scrollspy = ScrollSpy.getOrCreateInstance(div, { offset: 1 }) expect(scrollspy).toBeInstanceOf(ScrollSpy) expect(scrollspy._config.offset).toEqual(1) }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div, { offset: 1 }) expect(ScrollSpy.getInstance(div)).toEqual(scrollspy) const scrollspy2 = ScrollSpy.getOrCreateInstance(div, { offset: 2 }) expect(scrollspy).toBeInstanceOf(ScrollSpy) expect(scrollspy2).toEqual(scrollspy) expect(scrollspy2._config.offset).toEqual(1) }) }) describe('event handler', () => { it('should create scrollspy on window load event', () => { fixtureEl.innerHTML = [ '' + '
' ].join('') const scrollSpyEl = fixtureEl.querySelector('#wrapper') window.dispatchEvent(createEvent('load')) expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull() }) }) describe('SmoothScroll', () => { it('should not enable smoothScroll', () => { fixtureEl.innerHTML = getDummyFixture() const offSpy = spyOn(EventHandler, 'off').and.callThrough() const onSpy = spyOn(EventHandler, 'on').and.callThrough() const div = fixtureEl.querySelector('.content') const target = fixtureEl.querySelector('#navBar') // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1 }) expect(offSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') expect(onSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') }) it('should enable smoothScroll', () => { fixtureEl.innerHTML = getDummyFixture() const offSpy = spyOn(EventHandler, 'off').and.callThrough() const onSpy = spyOn(EventHandler, 'on').and.callThrough() const div = fixtureEl.querySelector('.content') const target = fixtureEl.querySelector('#navBar') // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1, smoothScroll: true }) expect(offSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy') expect(onSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy', '[href]', jasmine.any(Function)) }) it('should not smoothScroll to element if it not handles a scrollspy section', () => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
' ].join('') const div = fixtureEl.querySelector('.content') // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1, smoothScroll: true }) const clickSpy = getElementScrollSpy(div) fixtureEl.querySelector('#anchor-2').click() expect(clickSpy).not.toHaveBeenCalled() }) it('should call `scrollTop` if element doesn\'t not support `scrollTo`', () => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const link = fixtureEl.querySelector('[href="#div-jsm-1"]') delete div.scrollTo const clickSpy = getElementScrollSpy(div) // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1, smoothScroll: true }) link.click() expect(clickSpy).toHaveBeenCalled() }) it('should smoothScroll to the proper observable element on anchor click', done => { fixtureEl.innerHTML = getDummyFixture() const div = fixtureEl.querySelector('.content') const link = fixtureEl.querySelector('[href="#div-jsm-1"]') const observable = fixtureEl.querySelector('#div-jsm-1') const clickSpy = getElementScrollSpy(div) // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1, smoothScroll: true }) setTimeout(() => { if (div.scrollTo) { expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' }) } else { expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop) } done() }, 100) link.click() }) it('should smoothscroll to observable with anchor link that contains a french word as id', done => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
' ].join('') const div = fixtureEl.querySelector('.content') const link = fixtureEl.querySelector('[href="#présentation"]') const observable = fixtureEl.querySelector('#présentation') const clickSpy = getElementScrollSpy(div) // eslint-disable-next-line no-new new ScrollSpy(div, { offset: 1, smoothScroll: true }) setTimeout(() => { if (div.scrollTo) { expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' }) } else { expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop) } done() }, 100) link.click() }) }) }) ================================================ FILE: js/tests/unit/tab.spec.js ================================================ import Tab from '../../src/tab.js' import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Tab', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Tab.VERSION).toEqual(jasmine.any(String)) }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ '', '
    ', '
  • ', '
' ].join('') const tabEl = fixtureEl.querySelector('[href="#home"]') const tabBySelector = new Tab('[href="#home"]') const tabByElement = new Tab(tabEl) expect(tabBySelector._element).toEqual(tabEl) expect(tabByElement._element).toEqual(tabEl) }) it('Do not Throw exception if not parent', () => { fixtureEl.innerHTML = [ fixtureEl.innerHTML = '
' ].join('') const navEl = fixtureEl.querySelector('.nav-link') expect(() => { new Tab(navEl) // eslint-disable-line no-new }).not.toThrowError(TypeError) }) }) describe('show', () => { it('should activate element by tab id (using buttons, the preferred semantic way)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
    ', '
  • ', '
  • ', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector('#profile')).toHaveClass('active') expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') resolve() }) tab.show() }) }) it('should activate element by tab id (using links for tabs - not ideal, but still supported)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
    ', '
  • ', '
  • ', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector('#profile')).toHaveClass('active') expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') resolve() }) tab.show() }) }) it('should activate element by tab id in ordered list', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
    ', '
  1. ', '
  2. ', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector('#profile')).toHaveClass('active') resolve() }) tab.show() }) }) it('should activate element by tab id in nav list', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector('#profile')).toHaveClass('active') resolve() }) tab.show() }) }) it('should activate element by tab id in list group', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', ' ', '
', '
', '
', '
', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector('#profile')).toHaveClass('active') resolve() }) tab.show() }) }) it('should work with tab id being an int', done => { fixtureEl.innerHTML = [ '', '
', '
', '
', ' Working Tab 1 (#tab1) Content Here', '
', '
', ' Working Tab 2 (#2) with numeric ID', '
', '
' ].join('') const profileTriggerEl = fixtureEl.querySelector('#trigger2') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelector(`#${CSS.escape('2')}`)).toHaveClass('active') done() }) tab.show() }) it('should not fire shown when show is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const navEl = fixtureEl.querySelector('.nav > div') const tab = new Tab(navEl) const expectDone = () => { setTimeout(() => { expect().nothing() resolve() }, 30) } navEl.addEventListener('show.bs.tab', ev => { ev.preventDefault() expectDone() }) navEl.addEventListener('shown.bs.tab', () => { reject(new Error('should not trigger shown event')) }) tab.show() }) }) it('should not fire shown when tab is already active', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const triggerActive = fixtureEl.querySelector('button.active') const tab = new Tab(triggerActive) triggerActive.addEventListener('shown.bs.tab', () => { reject(new Error('should not trigger shown event')) }) tab.show() setTimeout(() => { expect().nothing() resolve() }, 30) }) }) it('show and shown events should reference correct relatedTarget', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') const secondTab = new Tab(secondTabTrigger) secondTabTrigger.addEventListener('show.bs.tab', ev => { expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') }) secondTabTrigger.addEventListener('shown.bs.tab', ev => { expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') resolve() }) secondTab.show() }) }) it('should fire hide and hidden events', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const triggerList = fixtureEl.querySelectorAll('button') const firstTab = new Tab(triggerList[0]) const secondTab = new Tab(triggerList[1]) let hideCalled = false triggerList[0].addEventListener('shown.bs.tab', () => { secondTab.show() }) triggerList[0].addEventListener('hide.bs.tab', ev => { hideCalled = true expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') }) triggerList[0].addEventListener('hidden.bs.tab', ev => { expect(hideCalled).toBeTrue() expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') resolve() }) firstTab.show() }) }) it('should not fire hidden when hide is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '' ].join('') const triggerList = fixtureEl.querySelectorAll('button') const firstTab = new Tab(triggerList[0]) const secondTab = new Tab(triggerList[1]) const expectDone = () => { setTimeout(() => { expect().nothing() resolve() }, 30) } triggerList[0].addEventListener('shown.bs.tab', () => { secondTab.show() }) triggerList[0].addEventListener('hide.bs.tab', ev => { ev.preventDefault() expectDone() }) triggerList[0].addEventListener('hidden.bs.tab', () => { reject(new Error('should not trigger hidden')) }) firstTab.show() }) }) it('should handle removed tabs', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
test 1
', '
test 2
', '
test 3
', '
' ].join('') const secondNavEl = fixtureEl.querySelector('#secondNav') const btnCloseEl = fixtureEl.querySelector('#btnClose') const secondNavTab = new Tab(secondNavEl) secondNavEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelectorAll('.nav-tab')).toHaveSize(2) resolve() }) btnCloseEl.addEventListener('click', () => { const linkEl = btnCloseEl.parentNode const liEl = linkEl.parentNode const tabId = linkEl.getAttribute('href') const tabIdEl = fixtureEl.querySelector(tabId) liEl.remove() tabIdEl.remove() secondNavTab.show() }) btnCloseEl.click() }) }) it('should not focus on opened tab', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
    ', '
  • ', '
  • ', '
' ].join('') const firstTab = fixtureEl.querySelector('#home') firstTab.focus() const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { expect(document.activeElement).toBe(firstTab) expect(document.activeElement).not.toBe(profileTriggerEl) resolve() }) tab.show() }) }) }) describe('dispose', () => { it('should dispose a tab', () => { fixtureEl.innerHTML = '' const el = fixtureEl.querySelector('.nav > div') const tab = new Tab(fixtureEl.querySelector('.nav > div')) expect(Tab.getInstance(el)).not.toBeNull() tab.dispose() expect(Tab.getInstance(el)).toBeNull() }) }) describe('_activate', () => { it('should not be called if element argument is null', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl = fixtureEl.querySelector('.nav-link') const tab = new Tab(tabEl) const spy = jasmine.createSpy('spy') const spyQueue = spyOn(tab, '_queueCallback') tab._activate(null, spy) expect(spyQueue).not.toHaveBeenCalled() expect(spy).not.toHaveBeenCalled() }) }) describe('_setInitialAttributes', () => { it('should put aria attributes', () => { fixtureEl.innerHTML = [ '', '
', '
' ].join('') const tabEl = fixtureEl.querySelector('.nav-link') const parent = fixtureEl.querySelector('.nav') const children = fixtureEl.querySelectorAll('.nav-link') const tabPanel = fixtureEl.querySelector('#panel') const tabPanel2 = fixtureEl.querySelector('#panel2') expect(parent.getAttribute('role')).toEqual(null) expect(tabEl.getAttribute('role')).toEqual(null) expect(tabPanel.getAttribute('role')).toEqual(null) const tab = new Tab(tabEl) tab._setInitialAttributes(parent, children) expect(parent.getAttribute('role')).toEqual('tablist') expect(tabEl.getAttribute('role')).toEqual('tab') expect(tabPanel.getAttribute('role')).toEqual('tabpanel') expect(tabPanel2.getAttribute('role')).toEqual('tabpanel') expect(tabPanel.hasAttribute('tabindex')).toBeFalse() expect(tabPanel.hasAttribute('tabindex2')).toBeFalse() expect(tabPanel.getAttribute('aria-labelledby')).toEqual('foo') expect(tabPanel2.hasAttribute('aria-labelledby')).toBeFalse() }) }) describe('_keydown', () => { it('if event is not one of left/right/up/down arrow, ignore it', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl = fixtureEl.querySelector('.nav-link') const tab = new Tab(tabEl) const keydown = createEvent('keydown') keydown.key = 'Enter' const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() const spyKeydown = spyOn(tab, '_keydown') const spyGet = spyOn(tab, '_getChildren') tabEl.dispatchEvent(keydown) expect(spyKeydown).toHaveBeenCalled() expect(spyGet).not.toHaveBeenCalled() expect(spyStop).not.toHaveBeenCalled() expect(spyPrevent).not.toHaveBeenCalled() }) it('if keydown event is right/down arrow, handle it', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const tab3 = new Tab(tabEl3) const spyShow1 = spyOn(tab1, 'show').and.callThrough() const spyShow2 = spyOn(tab2, 'show').and.callThrough() const spyShow3 = spyOn(tab3, 'show').and.callThrough() const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() let keydown = createEvent('keydown') keydown.key = 'ArrowRight' tabEl1.dispatchEvent(keydown) expect(spyShow2).toHaveBeenCalled() expect(spyFocus2).toHaveBeenCalled() keydown = createEvent('keydown') keydown.key = 'ArrowDown' tabEl2.dispatchEvent(keydown) expect(spyShow3).toHaveBeenCalled() expect(spyFocus3).toHaveBeenCalled() tabEl3.dispatchEvent(keydown) expect(spyShow1).toHaveBeenCalled() expect(spyFocus1).toHaveBeenCalled() expect(spyStop).toHaveBeenCalledTimes(3) expect(spyPrevent).toHaveBeenCalledTimes(3) }) it('if keydown event is left arrow, handle it', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const spyShow1 = spyOn(tab1, 'show').and.callThrough() const spyShow2 = spyOn(tab2, 'show').and.callThrough() const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() let keydown = createEvent('keydown') keydown.key = 'ArrowLeft' tabEl2.dispatchEvent(keydown) expect(spyShow1).toHaveBeenCalled() expect(spyFocus1).toHaveBeenCalled() keydown = createEvent('keydown') keydown.key = 'ArrowUp' tabEl1.dispatchEvent(keydown) expect(spyShow2).toHaveBeenCalled() expect(spyFocus2).toHaveBeenCalled() expect(spyStop).toHaveBeenCalledTimes(2) expect(spyPrevent).toHaveBeenCalledTimes(2) }) it('if keydown event is Home, handle it', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl3 = fixtureEl.querySelector('#tab3') const tab3 = new Tab(tabEl3) tab3.show() const spyShown = jasmine.createSpy() tabEl1.addEventListener('shown.bs.tab', spyShown) const keydown = createEvent('keydown') keydown.key = 'Home' tabEl3.dispatchEvent(keydown) expect(spyShown).toHaveBeenCalled() }) it('if keydown event is End, handle it', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl3 = fixtureEl.querySelector('#tab3') const tab1 = new Tab(tabEl1) tab1.show() const spyShown = jasmine.createSpy() tabEl3.addEventListener('shown.bs.tab', spyShown) const keydown = createEvent('keydown') keydown.key = 'End' tabEl1.dispatchEvent(keydown) expect(spyShown).toHaveBeenCalled() }) it('if keydown event is right arrow and next element is disabled', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tabEl4 = fixtureEl.querySelector('#tab4') const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const tab3 = new Tab(tabEl3) const tab4 = new Tab(tabEl4) const spy1 = spyOn(tab1, 'show').and.callThrough() const spy2 = spyOn(tab2, 'show').and.callThrough() const spy3 = spyOn(tab3, 'show').and.callThrough() const spy4 = spyOn(tab4, 'show').and.callThrough() const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() const keydown = createEvent('keydown') keydown.key = 'ArrowRight' tabEl1.dispatchEvent(keydown) expect(spy1).not.toHaveBeenCalled() expect(spy2).not.toHaveBeenCalled() expect(spy3).not.toHaveBeenCalled() expect(spy4).toHaveBeenCalledTimes(1) expect(spyFocus1).not.toHaveBeenCalled() expect(spyFocus2).not.toHaveBeenCalled() expect(spyFocus3).not.toHaveBeenCalled() expect(spyFocus4).toHaveBeenCalledTimes(1) }) it('if keydown event is left arrow and next element is disabled', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tabEl4 = fixtureEl.querySelector('#tab4') const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const tab3 = new Tab(tabEl3) const tab4 = new Tab(tabEl4) const spy1 = spyOn(tab1, 'show').and.callThrough() const spy2 = spyOn(tab2, 'show').and.callThrough() const spy3 = spyOn(tab3, 'show').and.callThrough() const spy4 = spyOn(tab4, 'show').and.callThrough() const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() const keydown = createEvent('keydown') keydown.key = 'ArrowLeft' tabEl4.dispatchEvent(keydown) expect(spy4).not.toHaveBeenCalled() expect(spy3).not.toHaveBeenCalled() expect(spy2).not.toHaveBeenCalled() expect(spy1).toHaveBeenCalledTimes(1) expect(spyFocus4).not.toHaveBeenCalled() expect(spyFocus3).not.toHaveBeenCalled() expect(spyFocus2).not.toHaveBeenCalled() expect(spyFocus1).toHaveBeenCalledTimes(1) }) it('if keydown event is Home and first element is disabled', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tab3 = new Tab(tabEl3) tab3.show() const spyShown1 = jasmine.createSpy() const spyShown2 = jasmine.createSpy() tabEl1.addEventListener('shown.bs.tab', spyShown1) tabEl2.addEventListener('shown.bs.tab', spyShown2) const keydown = createEvent('keydown') keydown.key = 'Home' tabEl3.dispatchEvent(keydown) expect(spyShown1).not.toHaveBeenCalled() expect(spyShown2).toHaveBeenCalled() }) it('if keydown event is End and last element is disabled', () => { fixtureEl.innerHTML = [ '' ].join('') const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tab1 = new Tab(tabEl1) tab1.show() const spyShown2 = jasmine.createSpy() const spyShown3 = jasmine.createSpy() tabEl2.addEventListener('shown.bs.tab', spyShown2) tabEl3.addEventListener('shown.bs.tab', spyShown3) const keydown = createEvent('keydown') keydown.key = 'End' tabEl1.dispatchEvent(keydown) expect(spyShown3).not.toHaveBeenCalled() expect(spyShown2).toHaveBeenCalled() }) }) describe('jQueryInterface', () => { it('should create a tab', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('.nav > div') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tab.call(jQueryMock) expect(Tab.getInstance(div)).not.toBeNull() }) it('should not re create a tab', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tab.call(jQueryMock) expect(Tab.getInstance(div)).toEqual(tab) }) it('should call a tab method', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) const spy = spyOn(tab, 'show') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tab.call(jQueryMock, 'show') expect(Tab.getInstance(div)).toEqual(tab) expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('.nav > div') const action = 'undefinedMethod' jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.tab.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return null if there is no instance', () => { expect(Tab.getInstance(fixtureEl)).toBeNull() }) it('should return this instance', () => { fixtureEl.innerHTML = '' const divEl = fixtureEl.querySelector('.nav > div') const tab = new Tab(divEl) expect(Tab.getInstance(divEl)).toEqual(tab) expect(Tab.getInstance(divEl)).toBeInstanceOf(Tab) }) }) describe('getOrCreateInstance', () => { it('should return tab instance', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') const tab = new Tab(div) expect(Tab.getOrCreateInstance(div)).toEqual(tab) expect(Tab.getInstance(div)).toEqual(Tab.getOrCreateInstance(div, {})) expect(Tab.getOrCreateInstance(div)).toBeInstanceOf(Tab) }) it('should return new instance when there is no tab instance', () => { fixtureEl.innerHTML = '' const div = fixtureEl.querySelector('div') expect(Tab.getInstance(div)).toBeNull() expect(Tab.getOrCreateInstance(div)).toBeInstanceOf(Tab) }) }) describe('data-api', () => { it('should create dynamically a tab', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') secondTabTrigger.addEventListener('shown.bs.tab', () => { expect(secondTabTrigger).toHaveClass('active') expect(fixtureEl.querySelector('#profile')).toHaveClass('active') resolve() }) secondTabTrigger.click() }) }) it('selected tab should deactivate previous selected link in dropdown', () => { fixtureEl.innerHTML = [ '' ].join('') const firstLiLinkEl = fixtureEl.querySelector('li:first-child a') firstLiLinkEl.click() expect(firstLiLinkEl).toHaveClass('active') expect(fixtureEl.querySelector('li:last-child a')).not.toHaveClass('active') expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child')).not.toHaveClass('active') }) it('selecting a dropdown tab does not activate another', () => { const nav1 = [ '' ].join('') const nav2 = [ '' ].join('') fixtureEl.innerHTML = nav1 + nav2 const firstDropItem = fixtureEl.querySelector('#nav1 .dropdown-item') firstDropItem.click() expect(firstDropItem).toHaveClass('active') expect(fixtureEl.querySelector('#nav1 .dropdown-toggle')).toHaveClass('active') expect(fixtureEl.querySelector('#nav2 .dropdown-toggle')).not.toHaveClass('active') expect(fixtureEl.querySelector('#nav2 .dropdown-item')).not.toHaveClass('active') }) it('should support li > .dropdown-item', () => { fixtureEl.innerHTML = [ '' ].join('') const dropItems = fixtureEl.querySelectorAll('.dropdown-item') dropItems[1].click() expect(dropItems[0]).not.toHaveClass('active') expect(dropItems[1]).toHaveClass('active') expect(fixtureEl.querySelector('.nav-link')).not.toHaveClass('active') }) it('should handle nested tabs', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', ' ', '
', '
Nested Tab1 Content
', '
Nested Tab2 Content
', '
', '
', '
Tab2 Content
', '
Tab3 Content
', '
' ].join('') const tab1El = fixtureEl.querySelector('#tab1') const tabNested2El = fixtureEl.querySelector('#tabNested2') const xTab1El = fixtureEl.querySelector('#x-tab1') tabNested2El.addEventListener('shown.bs.tab', () => { expect(xTab1El).toHaveClass('active') resolve() }) tab1El.addEventListener('shown.bs.tab', () => { expect(xTab1El).toHaveClass('active') tabNested2El.click() }) tab1El.click() }) }) it('should not remove fade class if no active pane is present', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') const tabProfileEl = fixtureEl.querySelector('#profile') const tabHomeEl = fixtureEl.querySelector('#home') triggerTabHomeEl.addEventListener('shown.bs.tab', () => { setTimeout(() => { expect(tabProfileEl).toHaveClass('fade') expect(tabProfileEl).not.toHaveClass('show') expect(tabHomeEl).toHaveClass('fade') expect(tabHomeEl).toHaveClass('show') resolve() }, 10) }) triggerTabProfileEl.addEventListener('shown.bs.tab', () => { setTimeout(() => { expect(tabProfileEl).toHaveClass('fade') expect(tabProfileEl).toHaveClass('show') triggerTabHomeEl.click() }, 10) }) triggerTabProfileEl.click() }) }) it('should add `show` class to tab panes if there is no `.fade` class', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
test 1
', '
test 2
', '
' ].join('') const secondNavEl = fixtureEl.querySelector('#secondNav') secondNavEl.addEventListener('shown.bs.tab', () => { expect(fixtureEl.querySelectorAll('.tab-content .show')).toHaveSize(1) resolve() }) secondNavEl.click() }) }) it('should add show class to tab panes if there is a `.fade` class', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
test 1
', '
test 2
', '
' ].join('') const secondNavEl = fixtureEl.querySelector('#secondNav') secondNavEl.addEventListener('shown.bs.tab', () => { setTimeout(() => { expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) resolve() }, 10) }) secondNavEl.click() }) }) it('should prevent default when the trigger is or ', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '' ].join('') const tabEl = fixtureEl.querySelector('[href="#test2"]') const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() tabEl.addEventListener('shown.bs.tab', () => { expect(tabEl).toHaveClass('active') expect(spy).toHaveBeenCalled() resolve() }) tabEl.click() }) }) it('should not fire shown when tab has disabled attribute', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const triggerDisabled = fixtureEl.querySelector('button[disabled]') triggerDisabled.addEventListener('shown.bs.tab', () => { reject(new Error('should not trigger shown event')) }) triggerDisabled.click() setTimeout(() => { expect().nothing() resolve() }, 30) }) }) it('should not fire shown when tab has disabled class', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const triggerDisabled = fixtureEl.querySelector('a.disabled') triggerDisabled.addEventListener('shown.bs.tab', () => { reject(new Error('should not trigger shown event')) }) triggerDisabled.click() setTimeout(() => { expect().nothing() resolve() }, 30) }) }) }) }) ================================================ FILE: js/tests/unit/toast.spec.js ================================================ import Toast from '../../src/toast.js' import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Toast', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Toast.VERSION).toEqual(jasmine.any(String)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Toast.DATA_KEY).toEqual('bs.toast') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '
' const toastEl = fixtureEl.querySelector('.toast') const toastBySelector = new Toast('.toast') const toastByElement = new Toast(toastEl) expect(toastBySelector._element).toEqual(toastEl) expect(toastByElement._element).toEqual(toastEl) }) it('should allow to config in js', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl, { delay: 1 }) toastEl.addEventListener('shown.bs.toast', () => { expect(toastEl).toHaveClass('show') resolve() }) toast.show() }) }) it('should close toast when close element with data-bs-dismiss attribute is set', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) toastEl.addEventListener('shown.bs.toast', () => { expect(toastEl).toHaveClass('show') const button = toastEl.querySelector('.btn-close') button.click() }) toastEl.addEventListener('hidden.bs.toast', () => { expect(toastEl).not.toHaveClass('show') resolve() }) toast.show() }) }) }) describe('Default', () => { it('should expose default setting to allow to override them', () => { const defaultDelay = 1000 Toast.Default.delay = defaultDelay fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) expect(toast._config.delay).toEqual(defaultDelay) }) }) describe('DefaultType', () => { it('should expose default setting types for read', () => { expect(Toast.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('show', () => { it('should auto hide', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) toastEl.addEventListener('hidden.bs.toast', () => { expect(toastEl).not.toHaveClass('show') resolve() }) toast.show() }) }) it('should not add fade class', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) toastEl.addEventListener('shown.bs.toast', () => { expect(toastEl).not.toHaveClass('fade') resolve() }) toast.show() }) }) it('should not trigger shown if show is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) const assertDone = () => { setTimeout(() => { expect(toastEl).not.toHaveClass('show') resolve() }, 20) } toastEl.addEventListener('show.bs.toast', event => { event.preventDefault() assertDone() }) toastEl.addEventListener('shown.bs.toast', () => { reject(new Error('shown event should not be triggered if show is prevented')) }) toast.show() }) }) it('should clear timeout if toast is shown again before it is hidden', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) setTimeout(() => { toast._config.autohide = false toastEl.addEventListener('shown.bs.toast', () => { expect(spy).toHaveBeenCalled() expect(toast._timeout).toBeNull() resolve() }) toast.show() }, toast._config.delay / 2) const spy = spyOn(toast, '_clearTimeout').and.callThrough() toast.show() }) }) it('should clear timeout if toast is interacted with mouse', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) const spy = spyOn(toast, '_clearTimeout').and.callThrough() setTimeout(() => { spy.calls.reset() toastEl.addEventListener('mouseover', () => { expect(toast._clearTimeout).toHaveBeenCalledTimes(1) expect(toast._timeout).toBeNull() resolve() }) const mouseOverEvent = createEvent('mouseover') toastEl.dispatchEvent(mouseOverEvent) }, toast._config.delay / 2) toast.show() }) }) it('should clear timeout if toast is interacted with keyboard', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', ' a simple toast', ' ', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) const spy = spyOn(toast, '_clearTimeout').and.callThrough() setTimeout(() => { spy.calls.reset() toastEl.addEventListener('focusin', () => { expect(toast._clearTimeout).toHaveBeenCalledTimes(1) expect(toast._timeout).toBeNull() resolve() }) const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() }, toast._config.delay / 2) toast.show() }) }) it('should still auto hide after being interacted with mouse and keyboard', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', ' a simple toast', ' ', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) setTimeout(() => { toastEl.addEventListener('mouseover', () => { const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() }) toastEl.addEventListener('focusin', () => { const mouseOutEvent = createEvent('mouseout') toastEl.dispatchEvent(mouseOutEvent) }) toastEl.addEventListener('mouseout', () => { const outsideFocusable = document.getElementById('outside-focusable') outsideFocusable.focus() }) toastEl.addEventListener('focusout', () => { expect(toast._timeout).not.toBeNull() resolve() }) const mouseOverEvent = createEvent('mouseover') toastEl.dispatchEvent(mouseOverEvent) }, toast._config.delay / 2) toast.show() }) }) it('should not auto hide if focus leaves but mouse pointer remains inside', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', ' a simple toast', ' ', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) setTimeout(() => { toastEl.addEventListener('mouseover', () => { const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() }) toastEl.addEventListener('focusin', () => { const outsideFocusable = document.getElementById('outside-focusable') outsideFocusable.focus() }) toastEl.addEventListener('focusout', () => { expect(toast._timeout).toBeNull() resolve() }) const mouseOverEvent = createEvent('mouseover') toastEl.dispatchEvent(mouseOverEvent) }, toast._config.delay / 2) toast.show() }) }) it('should not auto hide if mouse pointer leaves but focus remains inside', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
', ' a simple toast', ' ', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) setTimeout(() => { toastEl.addEventListener('mouseover', () => { const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() }) toastEl.addEventListener('focusin', () => { const mouseOutEvent = createEvent('mouseout') toastEl.dispatchEvent(mouseOutEvent) }) toastEl.addEventListener('mouseout', () => { expect(toast._timeout).toBeNull() resolve() }) const mouseOverEvent = createEvent('mouseover') toastEl.dispatchEvent(mouseOverEvent) }, toast._config.delay / 2) toast.show() }) }) }) describe('hide', () => { it('should allow to hide toast manually', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) toastEl.addEventListener('shown.bs.toast', () => { toast.hide() }) toastEl.addEventListener('hidden.bs.toast', () => { expect(toastEl).not.toHaveClass('show') resolve() }) toast.show() }) }) it('should do nothing when we call hide on a non shown toast', () => { fixtureEl.innerHTML = '
' const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) const spy = spyOn(toastEl.classList, 'contains') toast.hide() expect(spy).toHaveBeenCalled() }) it('should not trigger hidden if hide is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) const assertDone = () => { setTimeout(() => { expect(toastEl).toHaveClass('show') resolve() }, 20) } toastEl.addEventListener('shown.bs.toast', () => { toast.hide() }) toastEl.addEventListener('hide.bs.toast', event => { event.preventDefault() assertDone() }) toastEl.addEventListener('hidden.bs.toast', () => { reject(new Error('hidden event should not be triggered if hide is prevented')) }) toast.show() }) }) }) describe('dispose', () => { it('should allow to destroy toast', () => { fixtureEl.innerHTML = '
' const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) expect(Toast.getInstance(toastEl)).not.toBeNull() toast.dispose() expect(Toast.getInstance(toastEl)).toBeNull() }) it('should allow to destroy toast and hide it before that', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', ' a simple toast', '
', '
' ].join('') const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) const expected = () => { expect(toastEl).toHaveClass('show') expect(Toast.getInstance(toastEl)).not.toBeNull() toast.dispose() expect(Toast.getInstance(toastEl)).toBeNull() expect(toastEl).not.toHaveClass('show') resolve() } toastEl.addEventListener('shown.bs.toast', () => { setTimeout(expected, 1) }) toast.show() }) }) }) describe('jQueryInterface', () => { it('should create a toast', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.toast = Toast.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.toast.call(jQueryMock) expect(Toast.getInstance(div)).not.toBeNull() }) it('should not re create a toast', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const toast = new Toast(div) jQueryMock.fn.toast = Toast.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.toast.call(jQueryMock) expect(Toast.getInstance(div)).toEqual(toast) }) it('should call a toast method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const toast = new Toast(div) const spy = spyOn(toast, 'show') jQueryMock.fn.toast = Toast.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.toast.call(jQueryMock, 'show') expect(Toast.getInstance(div)).toEqual(toast) expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.toast = Toast.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.toast.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) describe('getInstance', () => { it('should return a toast instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const toast = new Toast(div) expect(Toast.getInstance(div)).toEqual(toast) expect(Toast.getInstance(div)).toBeInstanceOf(Toast) }) it('should return null when there is no toast instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Toast.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return toast instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const toast = new Toast(div) expect(Toast.getOrCreateInstance(div)).toEqual(toast) expect(Toast.getInstance(div)).toEqual(Toast.getOrCreateInstance(div, {})) expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast) }) it('should return new instance when there is no toast instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Toast.getInstance(div)).toBeNull() expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast) }) it('should return new instance when there is no toast instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Toast.getInstance(div)).toBeNull() const toast = Toast.getOrCreateInstance(div, { delay: 1 }) expect(toast).toBeInstanceOf(Toast) expect(toast._config.delay).toEqual(1) }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const toast = new Toast(div, { delay: 1 }) expect(Toast.getInstance(div)).toEqual(toast) const toast2 = Toast.getOrCreateInstance(div, { delay: 2 }) expect(toast).toBeInstanceOf(Toast) expect(toast2).toEqual(toast) expect(toast2._config.delay).toEqual(1) }) }) }) ================================================ FILE: js/tests/unit/tooltip.spec.js ================================================ import EventHandler from '../../src/dom/event-handler.js' import Tooltip from '../../src/tooltip.js' import { noop } from '../../src/util/index.js' import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Tooltip', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() for (const tooltipEl of document.querySelectorAll('.tooltip')) { tooltipEl.remove() } }) describe('VERSION', () => { it('should return plugin version', () => { expect(Tooltip.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Tooltip.Default).toEqual(jasmine.any(Object)) }) }) describe('NAME', () => { it('should return plugin name', () => { expect(Tooltip.NAME).toEqual(jasmine.any(String)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Tooltip.DATA_KEY).toEqual('bs.tooltip') }) }) describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(Tooltip.EVENT_KEY).toEqual('.bs.tooltip') }) }) describe('DefaultType', () => { it('should return plugin default type', () => { expect(Tooltip.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('#tooltipEl') const tooltipBySelector = new Tooltip('#tooltipEl') const tooltipByElement = new Tooltip(tooltipEl) expect(tooltipBySelector._element).toEqual(tooltipEl) expect(tooltipByElement._element).toEqual(tooltipEl) }) it('should not take care of disallowed data attributes', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._config.sanitize).toBeTrue() }) it('should convert title and content to string if numbers', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { title: 1, content: 7 }) expect(tooltip._config.title).toEqual('1') expect(tooltip._config.content).toEqual('7') }) it('should enable selector delegation', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const containerEl = fixtureEl.querySelector('div') const tooltipContainer = new Tooltip(containerEl, { selector: 'a[rel="tooltip"]', trigger: 'click' }) containerEl.innerHTML = '' const tooltipInContainerEl = containerEl.querySelector('a') tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() tooltipContainer.dispose() resolve() }) tooltipInContainerEl.click() }) }) it('should create offset modifier when offset is passed as a function', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { offset: getOffset, popperConfig: { onFirstUpdate(state) { expect(getOffset).toHaveBeenCalledWith({ popper: state.rects.popper, reference: state.rects.reference, placement: state.placement }, tooltipEl) resolve() } } }) const offset = tooltip._getOffset() expect(offset).toEqual(jasmine.any(Function)) tooltip.show() }) }) it('should create offset modifier when offset option is passed in data attribute', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._getOffset()).toEqual([10, 20]) }) it('should allow to pass config to Popper with `popperConfig`', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { popperConfig: { placement: 'left' } }) const popperConfig = tooltip._getPopperConfig('top') expect(popperConfig.placement).toEqual('left') }) it('should allow to pass config to Popper with `popperConfig` as a function', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' }) const tooltip = new Tooltip(tooltipEl, { popperConfig: getPopperConfig }) const popperConfig = tooltip._getPopperConfig('top') // Ensure that the function was called with the default config. expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({ placement: jasmine.any(String) })) expect(popperConfig.placement).toEqual('left') }) it('should use original title, if not "data-bs-title" is given', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._getTitle()).toEqual('Another tooltip') }) }) describe('enable', () => { it('should enable a tooltip', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.enable() tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() resolve() }) tooltip.show() }) }) }) describe('disable', () => { it('should disable tooltip', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.disable() tooltipEl.addEventListener('show.bs.tooltip', () => { reject(new Error('should not show a disabled tooltip')) }) tooltip.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) }) describe('toggleEnabled', () => { it('should toggle enabled', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._isEnabled).toBeTrue() tooltip.toggleEnabled() expect(tooltip._isEnabled).toBeFalse() }) }) describe('toggle', () => { it('should do nothing if disabled', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.disable() tooltipEl.addEventListener('show.bs.tooltip', () => { reject(new Error('should not show a disabled tooltip')) }) tooltip.toggle() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) it('should show a tooltip', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() resolve() }) tooltip.toggle() }) }) it('should call toggle and show the tooltip when trigger is "click"', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { trigger: 'click' }) const spy = spyOn(tooltip, 'toggle').and.callThrough() tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(spy).toHaveBeenCalled() resolve() }) tooltipEl.click() }) }) it('should hide a tooltip', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltip.toggle() }) tooltipEl.addEventListener('hidden.bs.tooltip', () => { expect(document.querySelector('.tooltip')).toBeNull() resolve() }) tooltip.toggle() }) }) it('should call toggle and hide the tooltip when trigger is "click"', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { trigger: 'click' }) const spy = spyOn(tooltip, 'toggle').and.callThrough() tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipEl.click() }) tooltipEl.addEventListener('hidden.bs.tooltip', () => { expect(spy).toHaveBeenCalled() resolve() }) tooltipEl.click() }) }) }) describe('dispose', () => { it('should destroy a tooltip', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const addEventSpy = spyOn(tooltipEl, 'addEventListener').and.callThrough() const removeEventSpy = spyOn(tooltipEl, 'removeEventListener').and.callThrough() const tooltip = new Tooltip(tooltipEl) expect(Tooltip.getInstance(tooltipEl)).toEqual(tooltip) const expectedArgs = [ ['mouseover', jasmine.any(Function), jasmine.any(Boolean)], ['mouseout', jasmine.any(Function), jasmine.any(Boolean)], ['focusin', jasmine.any(Function), jasmine.any(Boolean)], ['focusout', jasmine.any(Function), jasmine.any(Boolean)] ] expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs) tooltip.dispose() expect(Tooltip.getInstance(tooltipEl)).toBeNull() expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs) }) it('should destroy a tooltip after it is shown and hidden', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltip.hide() }) tooltipEl.addEventListener('hidden.bs.tooltip', () => { tooltip.dispose() expect(tooltip.tip).toBeNull() expect(Tooltip.getInstance(tooltipEl)).toBeNull() resolve() }) tooltip.show() }) }) it('should destroy a tooltip and remove it from the dom', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() tooltip.dispose() expect(document.querySelector('.tooltip')).toBeNull() resolve() }) tooltip.show() }) }) it('should destroy a tooltip and reset it\'s initial title', () => { fixtureEl.innerHTML = [ '', '' ].join('') const tooltipWithTitleEl = fixtureEl.querySelector('#tooltipWithTitle') const tooltip = new Tooltip('#tooltipWithTitle') expect(tooltipWithTitleEl.getAttribute('title')).toBeNull() tooltip.dispose() expect(tooltipWithTitleEl.getAttribute('title')).toBe('tooltipTitle') const tooltipWithoutTitleEl = fixtureEl.querySelector('#tooltipWithoutTitle') const tooltip2 = new Tooltip('#tooltipWithTitle') expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull() tooltip2.dispose() expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull() }) }) describe('show', () => { it('should show a tooltip', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() expect(tooltipEl.getAttribute('aria-describedby')).toEqual(tooltipShown.getAttribute('id')) expect(tooltipShown.getAttribute('id')).toContain('tooltip') resolve() }) tooltip.show() }) }) it('should show a tooltip when hovering a child element', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', ' ', ' ', ' ', ' ', '' ].join('') const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const spy = spyOn(tooltip, 'show') tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) setTimeout(() => { expect(spy).toHaveBeenCalled() resolve() }, 0) }) }) it('should show a tooltip on mobile', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) document.documentElement.ontouchstart = noop const spy = spyOn(EventHandler, 'on').and.callThrough() tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) document.documentElement.ontouchstart = undefined resolve() }) tooltip.show() }) }) it('should show a tooltip relative to placement option', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { placement: 'bottom' }) tooltipEl.addEventListener('inserted.bs.tooltip', () => { expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') }) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom') resolve() }) tooltip.show() }) }) it('should not error when trying to show a tooltip that has been removed from the dom', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const firstCallback = () => { tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) let tooltipShown = document.querySelector('.tooltip') tooltipShown.remove() tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() resolve() }) tooltip.show() } tooltipEl.addEventListener('shown.bs.tooltip', firstCallback) tooltip.show() }) }) it('should show a tooltip with a dom element container', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { container: fixtureEl }) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() resolve() }) tooltip.show() }) }) it('should show a tooltip with a jquery element container', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { container: { 0: fixtureEl, jquery: 'jQuery' } }) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() resolve() }) tooltip.show() }) }) it('should show a tooltip with a selector in container', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { container: '#fixture' }) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() resolve() }) tooltip.show() }) }) it('should show a tooltip with placement as a function', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const spy = jasmine.createSpy('placement').and.returnValue('top') const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { placement: spy }) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(document.querySelector('.tooltip')).not.toBeNull() expect(spy).toHaveBeenCalled() resolve() }) tooltip.show() }) }) it('should show a tooltip without the animation', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tip = document.querySelector('.tooltip') expect(tip).not.toBeNull() expect(tip).not.toHaveClass('fade') resolve() }) tooltip.show() }) }) it('should throw an error the element is not visible', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) try { tooltip.show() } catch (error) { expect(error.message).toEqual('Please use show on visible elements') } }) it('should not show a tooltip if show.bs.tooltip is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const expectedDone = () => { setTimeout(() => { expect(document.querySelector('.tooltip')).toBeNull() resolve() }, 10) } tooltipEl.addEventListener('show.bs.tooltip', ev => { ev.preventDefault() expectedDone() }) tooltipEl.addEventListener('shown.bs.tooltip', () => { reject(new Error('Tooltip should not be shown')) }) tooltip.show() }) }) it('should show tooltip if leave event hasn\'t occurred before delay expires', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { delay: 150 }) const spy = spyOn(tooltip, 'show') setTimeout(() => { expect(spy).not.toHaveBeenCalled() }, 100) setTimeout(() => { expect(spy).toHaveBeenCalled() resolve() }, 200) tooltipEl.dispatchEvent(createEvent('mouseover')) }) }) it('should not show tooltip if leave event occurs before delay expires', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { delay: 150 }) const spy = spyOn(tooltip, 'show') setTimeout(() => { expect(spy).not.toHaveBeenCalled() tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) setTimeout(() => { expect(spy).toHaveBeenCalled() expect(document.querySelectorAll('.tooltip')).toHaveSize(0) resolve() }, 200) tooltipEl.dispatchEvent(createEvent('mouseover')) }) }) it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._config.delay).toEqual({ show: 0, hide: 150 }) setTimeout(() => { expect(tooltip._getTipElement()).toHaveClass('show') tooltipEl.dispatchEvent(createEvent('mouseout')) setTimeout(() => { expect(tooltip._getTipElement()).toHaveClass('show') tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) setTimeout(() => { expect(tooltip._getTipElement()).toHaveClass('show') expect(document.querySelectorAll('.tooltip')).toHaveSize(1) resolve() }, 200) }, 10) tooltipEl.dispatchEvent(createEvent('mouseover')) }) }) it('should not hide tooltip if leave event occurs and interaction remains inside trigger', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', 'Trigger', 'the tooltip', '' ].join('') const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const triggerChild = tooltipEl.querySelector('b') const spy = spyOn(tooltip, 'hide').and.callThrough() tooltipEl.addEventListener('mouseover', () => { const moveMouseToChildEvent = createEvent('mouseout') Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { value: triggerChild }) tooltipEl.dispatchEvent(moveMouseToChildEvent) }) tooltipEl.addEventListener('mouseout', () => { expect(spy).not.toHaveBeenCalled() resolve() }) tooltipEl.dispatchEvent(createEvent('mouseover')) }) }) it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', () => { return new Promise(resolve => { // Style this tooltip to give it plenty of room for popper to do what it wants fixtureEl.innerHTML = 'Trigger' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.15s', transitionDelay: '0s' }) setTimeout(() => { expect(tooltip._popper).not.toBeNull() expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') tooltipEl.dispatchEvent(createEvent('mouseout')) setTimeout(() => { expect(tooltip._getTipElement()).not.toHaveClass('show') tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) setTimeout(() => { expect(tooltip._popper).not.toBeNull() expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') resolve() }, 200) }, 10) tooltipEl.dispatchEvent(createEvent('mouseover')) }) }) it('should only trigger inserted event if a new tooltip element was created', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.15s', transitionDelay: '0s' }) const insertedFunc = jasmine.createSpy() tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) setTimeout(() => { expect(insertedFunc).toHaveBeenCalledTimes(1) tooltip.hide() setTimeout(() => { tooltip.show() }, 100) setTimeout(() => { expect(insertedFunc).toHaveBeenCalledTimes(2) resolve() }, 200) }, 0) tooltip.show() }) }) it('should show a tooltip with custom class provided in data attributes', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tip = document.querySelector('.tooltip') expect(tip).not.toBeNull() expect(tip).toHaveClass('custom-class') resolve() }) tooltip.show() }) }) it('should show a tooltip with custom class provided as a string in config', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { customClass: 'custom-class custom-class-2' }) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tip = document.querySelector('.tooltip') expect(tip).not.toBeNull() expect(tip).toHaveClass('custom-class') expect(tip).toHaveClass('custom-class-2') resolve() }) tooltip.show() }) }) it('should show a tooltip with custom class provided as a function in config', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const spy = jasmine.createSpy('customClass').and.callFake(function (el) { return `${el.dataset.classA} ${this.dataset.classB}` }) const tooltip = new Tooltip(tooltipEl, { customClass: spy }) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tip = document.querySelector('.tooltip') expect(tip).not.toBeNull() expect(spy).toHaveBeenCalled() expect(tip).toHaveClass('custom-class-a') expect(tip).toHaveClass('custom-class-b') resolve() }) tooltip.show() }) }) it('should remove `title` attribute if exists', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { expect(tooltipEl.getAttribute('title')).toBeNull() resolve() }) tooltip.show() }) }) }) describe('hide', () => { it('should hide a tooltip', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) tooltipEl.addEventListener('hidden.bs.tooltip', () => { expect(document.querySelector('.tooltip')).toBeNull() expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() resolve() }) tooltip.show() }) }) it('should hide a tooltip on mobile', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const spy = spyOn(EventHandler, 'off') tooltipEl.addEventListener('shown.bs.tooltip', () => { document.documentElement.ontouchstart = noop tooltip.hide() }) tooltipEl.addEventListener('hidden.bs.tooltip', () => { expect(document.querySelector('.tooltip')).toBeNull() expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) document.documentElement.ontouchstart = undefined resolve() }) tooltip.show() }) }) it('should hide a tooltip without animation', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) tooltipEl.addEventListener('hidden.bs.tooltip', () => { expect(document.querySelector('.tooltip')).toBeNull() expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() resolve() }) tooltip.show() }) }) it('should not hide a tooltip if hide event is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' const assertDone = () => { setTimeout(() => { expect(document.querySelector('.tooltip')).not.toBeNull() resolve() }, 20) } const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) tooltipEl.addEventListener('hide.bs.tooltip', event => { event.preventDefault() assertDone() }) tooltipEl.addEventListener('hidden.bs.tooltip', () => { reject(new Error('should not trigger hidden event')) }) tooltip.show() }) }) it('should not throw error running hide if popper hasn\'t been shown', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div) try { tooltip.hide() expect().nothing() } catch { throw new Error('should not throw error') } }) }) describe('update', () => { it('should call popper update', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const spy = spyOn(tooltip._popper, 'update') tooltip.update() expect(spy).toHaveBeenCalled() resolve() }) tooltip.show() }) }) it('should do nothing if the tooltip is not shown', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.update() expect().nothing() }) }) describe('_isWithContent', () => { it('should return true if there is content', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._isWithContent()).toBeTrue() }) it('should return false if there is no content', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._isWithContent()).toBeFalse() }) }) describe('_getTipElement', () => { it('should create the tip element and return it', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const spy = spyOn(document, 'createElement').and.callThrough() expect(tooltip._getTipElement()).toBeDefined() expect(spy).toHaveBeenCalled() }) it('should return the created tip element', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const spy = spyOn(document, 'createElement').and.callThrough() expect(tooltip._getTipElement()).toBeDefined() expect(spy).toHaveBeenCalled() spy.calls.reset() expect(tooltip._getTipElement()).toBeDefined() expect(spy).not.toHaveBeenCalled() }) }) describe('setContent', () => { it('should set tip content', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) const tip = tooltip._getTipElement() tooltip.setContent(tip) expect(tip).not.toHaveClass('show') expect(tip).not.toHaveClass('fade') expect(tip.querySelector('.tooltip-inner').textContent).toEqual('Another tooltip') }) it('should re-show tip if it was already shown', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.show() const tip = () => tooltip._getTipElement() expect(tip()).toHaveClass('show') tooltip.setContent({ '.tooltip-inner': 'foo' }) expect(tip()).toHaveClass('show') expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo') }) it('should keep tip hidden, if it was already hidden before', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const tip = () => tooltip._getTipElement() expect(tip()).not.toHaveClass('show') tooltip.setContent({ '.tooltip-inner': 'foo' }) expect(tip()).not.toHaveClass('show') tooltip.show() expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo') }) it('"setContent" should keep the initial template', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.setContent({ '.tooltip-inner': 'foo' }) const tip = tooltip._getTipElement() expect(tip).toHaveClass('tooltip') expect(tip).toHaveClass('bs-tooltip-auto') expect(tip.querySelector('.tooltip-arrow')).not.toBeNull() expect(tip.querySelector('.tooltip-inner')).not.toBeNull() }) }) describe('setContent', () => { it('should do nothing if the element is null', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.setContent({ '.tooltip': null }) expect().nothing() }) it('should do nothing if the content is a child of the element', () => { fixtureEl.innerHTML = [ '', '
', '
' ].join('') const tooltipEl = fixtureEl.querySelector('a') const childContent = fixtureEl.querySelector('div') const tooltip = new Tooltip(tooltipEl, { html: true }) tooltip._getTipElement().append(childContent) tooltip.setContent({ '.tooltip': childContent }) expect().nothing() }) it('should add the content as a child of the element for jQuery elements', () => { fixtureEl.innerHTML = [ '', '
', '
' ].join('') const tooltipEl = fixtureEl.querySelector('a') const childContent = fixtureEl.querySelector('div') const tooltip = new Tooltip(tooltipEl, { html: true }) tooltip.setContent({ '.tooltip': { 0: childContent, jquery: 'jQuery' } }) tooltip.show() expect(childContent.parentNode).toEqual(tooltip._getTipElement()) }) it('should add the child text content in the element', () => { fixtureEl.innerHTML = [ '', '
Tooltip
', '
' ].join('') const tooltipEl = fixtureEl.querySelector('a') const childContent = fixtureEl.querySelector('div') const tooltip = new Tooltip(tooltipEl) tooltip.setContent({ '.tooltip': childContent }) expect(childContent.textContent).toEqual(tooltip._getTipElement().textContent) }) it('should add html without sanitize it', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { sanitize: false, html: true }) tooltip.setContent({ '.tooltip': '
Tooltip
' }) expect(tooltip._getTipElement().querySelector('div').id).toEqual('childContent') }) it('should add html sanitized', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { html: true }) const content = [ '
', ' ', '
' ].join('') tooltip.setContent({ '.tooltip': content }) expect(tooltip._getTipElement().querySelector('div').id).toEqual('childContent') expect(tooltip._getTipElement().querySelector('button')).toBeNull() }) it('should add text content', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.setContent({ '.tooltip': 'test' }) expect(tooltip._getTipElement().textContent).toEqual('test') }) }) describe('_getTitle', () => { it('should return the title', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) expect(tooltip._getTitle()).toEqual('Another tooltip') }) it('should call title function', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { title: () => 'test' }) expect(tooltip._getTitle()).toEqual('test') }) it('should call title function with trigger element', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { title(el) { return el.dataset.foo } }) expect(tooltip._getTitle()).toEqual('bar') }) it('should call title function with correct this value', () => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { title() { return this.dataset.foo } }) expect(tooltip._getTitle()).toEqual('bar') }) }) describe('getInstance', () => { it('should return tooltip instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const alert = new Tooltip(div) expect(Tooltip.getInstance(div)).toEqual(alert) expect(Tooltip.getInstance(div)).toBeInstanceOf(Tooltip) }) it('should return null when there is no tooltip instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Tooltip.getInstance(div)).toBeNull() }) }) describe('aria-label', () => { it('should add the aria-label attribute for referencing original title', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') resolve() }) tooltip.show() }) }) it('should add the aria-label attribute when element text content is a whitespace string', () => { return new Promise(resolve => { fixtureEl.innerHTML = ' ' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() expect(tooltipEl.getAttribute('aria-label')).toEqual('A tooltip') resolve() }) tooltip.show() }) }) it('should not add the aria-label attribute if the attribute already exists', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') resolve() }) tooltip.show() }) }) it('should not add the aria-label attribute if the element has text content', () => { return new Promise(resolve => { fixtureEl.innerHTML = 'text content' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() expect(tooltipEl.getAttribute('aria-label')).toBeNull() resolve() }) tooltip.show() }) }) }) describe('getOrCreateInstance', () => { it('should return tooltip instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div) expect(Tooltip.getOrCreateInstance(div)).toEqual(tooltip) expect(Tooltip.getInstance(div)).toEqual(Tooltip.getOrCreateInstance(div, {})) expect(Tooltip.getOrCreateInstance(div)).toBeInstanceOf(Tooltip) }) it('should return new instance when there is no tooltip instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Tooltip.getInstance(div)).toBeNull() expect(Tooltip.getOrCreateInstance(div)).toBeInstanceOf(Tooltip) }) it('should return new instance when there is no tooltip instance with given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Tooltip.getInstance(div)).toBeNull() const tooltip = Tooltip.getOrCreateInstance(div, { title: () => 'test' }) expect(tooltip).toBeInstanceOf(Tooltip) expect(tooltip._getTitle()).toEqual('test') }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div, { title: () => 'nothing' }) expect(Tooltip.getInstance(div)).toEqual(tooltip) const tooltip2 = Tooltip.getOrCreateInstance(div, { title: () => 'test' }) expect(tooltip).toBeInstanceOf(Tooltip) expect(tooltip2).toEqual(tooltip) expect(tooltip2._getTitle()).toEqual('nothing') }) }) describe('jQueryInterface', () => { it('should create a tooltip', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.tooltip = Tooltip.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tooltip.call(jQueryMock) expect(Tooltip.getInstance(div)).not.toBeNull() }) it('should not re create a tooltip', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div) jQueryMock.fn.tooltip = Tooltip.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tooltip.call(jQueryMock) expect(Tooltip.getInstance(div)).toEqual(tooltip) }) it('should call a tooltip method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div) const spy = spyOn(tooltip, 'show') jQueryMock.fn.tooltip = Tooltip.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.tooltip.call(jQueryMock, 'show') expect(Tooltip.getInstance(div)).toEqual(tooltip) expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' jQueryMock.fn.tooltip = Tooltip.jQueryInterface jQueryMock.elements = [div] expect(() => { jQueryMock.fn.tooltip.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) }) }) ================================================ FILE: js/tests/unit/util/backdrop.spec.js ================================================ import Backdrop from '../../../src/util/backdrop.js' import { getTransitionDurationFromElement } from '../../../src/util/index.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' const CLASS_BACKDROP = '.modal-backdrop' const CLASS_NAME_FADE = 'fade' const CLASS_NAME_SHOW = 'show' describe('Backdrop', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() const list = document.querySelectorAll(CLASS_BACKDROP) for (const el of list) { el.remove() } }) describe('show', () => { it('should append the backdrop html once on show and include the "show" class if it is "shown"', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, isAnimated: false }) const getElements = () => document.querySelectorAll(CLASS_BACKDROP) expect(getElements()).toHaveSize(0) instance.show() instance.show(() => { expect(getElements()).toHaveSize(1) for (const el of getElements()) { expect(el).toHaveClass(CLASS_NAME_SHOW) } resolve() }) }) }) it('should not append the backdrop html if it is not "shown"', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: false, isAnimated: true }) const getElements = () => document.querySelectorAll(CLASS_BACKDROP) expect(getElements()).toHaveSize(0) instance.show(() => { expect(getElements()).toHaveSize(0) resolve() }) }) }) it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, isAnimated: true }) const getElements = () => document.querySelectorAll(CLASS_BACKDROP) expect(getElements()).toHaveSize(0) instance.show(() => { expect(getElements()).toHaveSize(1) for (const el of getElements()) { expect(el).toHaveClass(CLASS_NAME_FADE) } resolve() }) }) }) }) describe('hide', () => { it('should remove the backdrop html', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, isAnimated: true }) const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) expect(getElements()).toHaveSize(0) instance.show(() => { expect(getElements()).toHaveSize(1) instance.hide(() => { expect(getElements()).toHaveSize(0) resolve() }) }) }) }) it('should remove the "show" class', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, isAnimated: true }) const elem = instance._getElement() instance.show() instance.hide(() => { expect(elem).not.toHaveClass(CLASS_NAME_SHOW) resolve() }) }) }) it('should not try to remove Node on remove method if it is not "shown"', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: false, isAnimated: true }) const getElements = () => document.querySelectorAll(CLASS_BACKDROP) const spy = spyOn(instance, 'dispose').and.callThrough() expect(getElements()).toHaveSize(0) expect(instance._isAppended).toBeFalse() instance.show(() => { instance.hide(() => { expect(getElements()).toHaveSize(0) expect(spy).not.toHaveBeenCalled() expect(instance._isAppended).toBeFalse() resolve() }) }) }) }) it('should not error if the backdrop no longer has a parent', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const wrapper = fixtureEl.querySelector('#wrapper') const instance = new Backdrop({ isVisible: true, isAnimated: true, rootElement: wrapper }) const getElements = () => document.querySelectorAll(CLASS_BACKDROP) instance.show(() => { wrapper.remove() instance.hide(() => { expect(getElements()).toHaveSize(0) resolve() }) }) }) }) }) describe('click callback', () => { it('should execute callback on click', () => { return new Promise(resolve => { const spy = jasmine.createSpy('spy') const instance = new Backdrop({ isVisible: true, isAnimated: false, clickCallback: () => spy() }) const endTest = () => { setTimeout(() => { expect(spy).toHaveBeenCalled() resolve() }, 10) } instance.show(() => { const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) endTest() }) }) }) describe('animation callbacks', () => { it('should show and hide backdrop after counting transition duration if it is animated', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, isAnimated: true }) const spy2 = jasmine.createSpy('spy2') const execDone = () => { setTimeout(() => { expect(spy2).toHaveBeenCalledTimes(2) resolve() }, 10) } instance.show(spy2) instance.hide(() => { spy2() execDone() }) expect(spy2).not.toHaveBeenCalled() }) }) it('should show and hide backdrop without a delay if it is not animated', () => { return new Promise(resolve => { const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) const instance = new Backdrop({ isVisible: true, isAnimated: false }) const spy2 = jasmine.createSpy('spy2') instance.show(spy2) instance.hide(spy2) setTimeout(() => { expect(spy2).toHaveBeenCalled() expect(spy).not.toHaveBeenCalled() resolve() }, 10) }) }) it('should not call delay callbacks if it is not "shown"', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: false, isAnimated: true }) const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) instance.show() instance.hide(() => { expect(spy).not.toHaveBeenCalled() resolve() }) }) }) }) describe('Config', () => { describe('rootElement initialization', () => { it('should be appended on "document.body" by default', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true }) const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { expect(getElement().parentElement).toEqual(document.body) resolve() }) }) }) it('should find the rootElement if passed as a string', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, rootElement: 'body' }) const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { expect(getElement().parentElement).toEqual(document.body) resolve() }) }) }) it('should be appended on any element given by the proper config', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const wrapper = fixtureEl.querySelector('#wrapper') const instance = new Backdrop({ isVisible: true, rootElement: wrapper }) const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { expect(getElement().parentElement).toEqual(wrapper) resolve() }) }) }) }) describe('ClassName', () => { it('should allow configuring className', () => { return new Promise(resolve => { const instance = new Backdrop({ isVisible: true, className: 'foo' }) const getElement = () => document.querySelector('.foo') instance.show(() => { expect(getElement()).toEqual(instance._getElement()) instance.dispose() resolve() }) }) }) }) }) }) }) ================================================ FILE: js/tests/unit/util/component-functions.spec.js ================================================ import BaseComponent from '../../../src/base-component.js' import { enableDismissTrigger } from '../../../src/util/component-functions.js' import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js' class DummyClass2 extends BaseComponent { static get NAME() { return 'test' } hide() { return true } testMethod() { return true } } describe('Plugin functions', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('data-bs-dismiss functionality', () => { it('should get Plugin and execute the given method, when a click occurred on data-bs-dismiss="PluginName"', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() const spyTest = spyOn(DummyClass2.prototype, 'testMethod') const componentWrapper = fixtureEl.querySelector('#foo') const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') enableDismissTrigger(DummyClass2, 'testMethod') btnClose.dispatchEvent(event) expect(spyGet).toHaveBeenCalledWith(componentWrapper) expect(spyTest).toHaveBeenCalled() }) it('if data-bs-dismiss="PluginName" hasn\'t got "data-bs-target", "getOrCreateInstance" has to be initialized by closest "plugin.Name" class', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() const spyHide = spyOn(DummyClass2.prototype, 'hide') const componentWrapper = fixtureEl.querySelector('#foo') const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') enableDismissTrigger(DummyClass2) btnClose.dispatchEvent(event) expect(spyGet).toHaveBeenCalledWith(componentWrapper) expect(spyHide).toHaveBeenCalled() }) it('if data-bs-dismiss="PluginName" is disabled, must not trigger function', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const spy = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') enableDismissTrigger(DummyClass2) btnClose.dispatchEvent(event) expect(spy).not.toHaveBeenCalled() }) it('should prevent default when the trigger is or ', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') enableDismissTrigger(DummyClass2) const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() btnClose.dispatchEvent(event) expect(spy).toHaveBeenCalled() }) }) }) ================================================ FILE: js/tests/unit/util/config.spec.js ================================================ import Config from '../../../src/util/config.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' class DummyConfigClass extends Config { static get NAME() { return 'dummy' } } describe('Config', () => { let fixtureEl const name = 'dummy' beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('NAME', () => { it('should return plugin NAME', () => { expect(DummyConfigClass.NAME).toEqual(name) }) }) describe('DefaultType', () => { it('should return plugin default type', () => { expect(DummyConfigClass.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('Default', () => { it('should return plugin defaults', () => { expect(DummyConfigClass.Default).toEqual(jasmine.any(Object)) }) }) describe('mergeConfigObj', () => { it('should parse element\'s data attributes and merge it with default config. Element\'s data attributes must excel Defaults', () => { fixtureEl.innerHTML = '
' spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ testBool: true, testString: 'foo', testString1: 'foo', testInt: 7 }) const instance = new DummyConfigClass() const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test')) expect(configResult.testBool).toEqual(false) expect(configResult.testString).toEqual('foo') expect(configResult.testString1).toEqual('bar') expect(configResult.testInt).toEqual(8) }) it('should parse element\'s data attributes and merge it with default config, plug these given during method call. The programmatically given should excel all', () => { fixtureEl.innerHTML = '
' spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ testBool: true, testString: 'foo', testString1: 'foo', testInt: 7 }) const instance = new DummyConfigClass() const configResult = instance._mergeConfigObj({ testString1: 'test', testInt: 3 }, fixtureEl.querySelector('#test')) expect(configResult.testBool).toEqual(false) expect(configResult.testString).toEqual('foo') expect(configResult.testString1).toEqual('test') expect(configResult.testInt).toEqual(3) }) it('should parse element\'s data attribute `config` and any rest attributes. The programmatically given should excel all. Data attribute `config` should excel only Defaults', () => { fixtureEl.innerHTML = '
' spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ testBool: true, testString: 'foo', testString1: 'foo', testInt: 7, testInt2: 600 }) const instance = new DummyConfigClass() const configResult = instance._mergeConfigObj({ testString1: 'test' }, fixtureEl.querySelector('#test')) expect(configResult.testBool).toEqual(false) expect(configResult.testString).toEqual('foo') expect(configResult.testString1).toEqual('test') expect(configResult.testInt).toEqual(8) expect(configResult.testInt2).toEqual(100) }) it('should omit element\'s data attribute `config` if is not an object', () => { fixtureEl.innerHTML = '
' spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ testInt: 7, testInt2: 79 }) const instance = new DummyConfigClass() const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test')) expect(configResult.testInt).toEqual(8) expect(configResult.testInt2).toEqual(79) }) }) describe('typeCheckConfig', () => { it('should check type of the config object', () => { spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ toggle: 'boolean', parent: '(string|element)' }) const config = { toggle: true, parent: 777 } const obj = new DummyConfigClass() expect(() => { obj._typeCheckConfig(config) }).toThrowError(TypeError, `${obj.constructor.NAME.toUpperCase()}: Option "parent" provided type "number" but expected type "(string|element)".`) }) it('should return null stringified when null is passed', () => { spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ toggle: 'boolean', parent: '(null|element)' }) const obj = new DummyConfigClass() const config = { toggle: true, parent: null } obj._typeCheckConfig(config) expect().nothing() }) it('should return undefined stringified when undefined is passed', () => { spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ toggle: 'boolean', parent: '(undefined|element)' }) const obj = new DummyConfigClass() const config = { toggle: true, parent: undefined } obj._typeCheckConfig(config) expect().nothing() }) }) }) ================================================ FILE: js/tests/unit/util/focustrap.spec.js ================================================ import EventHandler from '../../../src/dom/event-handler.js' import SelectorEngine from '../../../src/dom/selector-engine.js' import FocusTrap from '../../../src/util/focustrap.js' import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js' describe('FocusTrap', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('activate', () => { it('should autofocus itself by default', () => { fixtureEl.innerHTML = '
' const trapElement = fixtureEl.querySelector('div') const spy = spyOn(trapElement, 'focus') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() expect(spy).toHaveBeenCalled() }) it('if configured not to autofocus, should not autofocus itself', () => { fixtureEl.innerHTML = '
' const trapElement = fixtureEl.querySelector('div') const spy = spyOn(trapElement, 'focus') const focustrap = new FocusTrap({ trapElement, autofocus: false }) focustrap.activate() expect(spy).not.toHaveBeenCalled() }) it('should force focus inside focus trap if it can', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ 'outside', '
', ' inside', '
' ].join('') const trapElement = fixtureEl.querySelector('div') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() const inside = document.getElementById('inside') const focusInListener = () => { expect(spy).toHaveBeenCalled() document.removeEventListener('focusin', focusInListener) resolve() } const spy = spyOn(inside, 'focus') spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) document.addEventListener('focusin', focusInListener) const focusInEvent = createEvent('focusin', { bubbles: true }) Object.defineProperty(focusInEvent, 'target', { value: document.getElementById('outside') }) document.dispatchEvent(focusInEvent) }) }) it('should wrap focus around forward on tab', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ 'outside', '
', ' first', ' inside', ' last', '
' ].join('') const trapElement = fixtureEl.querySelector('div') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() const first = document.getElementById('first') const inside = document.getElementById('inside') const last = document.getElementById('last') const outside = document.getElementById('outside') spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) const spy = spyOn(first, 'focus').and.callThrough() const focusInListener = () => { expect(spy).toHaveBeenCalled() first.removeEventListener('focusin', focusInListener) resolve() } first.addEventListener('focusin', focusInListener) const keydown = createEvent('keydown') keydown.key = 'Tab' document.dispatchEvent(keydown) outside.focus() }) }) it('should wrap focus around backwards on shift-tab', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ 'outside', '
', ' first', ' inside', ' last', '
' ].join('') const trapElement = fixtureEl.querySelector('div') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() const first = document.getElementById('first') const inside = document.getElementById('inside') const last = document.getElementById('last') const outside = document.getElementById('outside') spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) const spy = spyOn(last, 'focus').and.callThrough() const focusInListener = () => { expect(spy).toHaveBeenCalled() last.removeEventListener('focusin', focusInListener) resolve() } last.addEventListener('focusin', focusInListener) const keydown = createEvent('keydown') keydown.key = 'Tab' keydown.shiftKey = true document.dispatchEvent(keydown) outside.focus() }) }) it('should force focus on itself if there is no focusable content', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ 'outside', '
' ].join('') const trapElement = fixtureEl.querySelector('div') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() const focusInListener = () => { expect(spy).toHaveBeenCalled() document.removeEventListener('focusin', focusInListener) resolve() } const spy = spyOn(focustrap._config.trapElement, 'focus') document.addEventListener('focusin', focusInListener) const focusInEvent = createEvent('focusin', { bubbles: true }) Object.defineProperty(focusInEvent, 'target', { value: document.getElementById('outside') }) document.dispatchEvent(focusInEvent) }) }) }) describe('deactivate', () => { it('should flag itself as no longer active', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) focustrap.activate() expect(focustrap._isActive).toBeTrue() focustrap.deactivate() expect(focustrap._isActive).toBeFalse() }) it('should remove all event listeners', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) focustrap.activate() const spy = spyOn(EventHandler, 'off') focustrap.deactivate() expect(spy).toHaveBeenCalled() }) it('doesn\'t try removing event listeners unless it needs to (in case it hasn\'t been activated)', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) const spy = spyOn(EventHandler, 'off') focustrap.deactivate() expect(spy).not.toHaveBeenCalled() }) }) }) ================================================ FILE: js/tests/unit/util/index.spec.js ================================================ import * as Util from '../../../src/util/index.js' import { noop } from '../../../src/util/index.js' import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Util', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('getUID', () => { it('should generate uid', () => { const uid = Util.getUID('bs') const uid2 = Util.getUID('bs') expect(uid).not.toEqual(uid2) }) }) describe('getTransitionDurationFromElement', () => { it('should get transition from element', () => { fixtureEl.innerHTML = '
' expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(300) }) it('should return 0 if the element is undefined or null', () => { expect(Util.getTransitionDurationFromElement(null)).toEqual(0) expect(Util.getTransitionDurationFromElement(undefined)).toEqual(0) }) it('should return 0 if the element do not possess transition', () => { fixtureEl.innerHTML = '
' expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(0) }) }) describe('triggerTransitionEnd', () => { it('should trigger transitionend event', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' const el = fixtureEl.querySelector('div') const spy = spyOn(el, 'dispatchEvent').and.callThrough() el.addEventListener('transitionend', () => { expect(spy).toHaveBeenCalled() resolve() }) Util.triggerTransitionEnd(el) }) }) }) describe('isElement', () => { it('should detect if the parameter is an element or not and return Boolean', () => { fixtureEl.innerHTML = [ '
', '
' ].join('') const el = fixtureEl.querySelector('#foo') expect(Util.isElement(el)).toBeTrue() expect(Util.isElement({})).toBeFalse() expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toBeFalse() }) it('should detect jQuery element', () => { fixtureEl.innerHTML = '
' const el = fixtureEl.querySelector('div') const fakejQuery = { 0: el, jquery: 'foo' } expect(Util.isElement(fakejQuery)).toBeTrue() }) }) describe('getElement', () => { it('should try to parse element', () => { fixtureEl.innerHTML = [ '
', '
' ].join('') const el = fixtureEl.querySelector('div') expect(Util.getElement(el)).toEqual(el) expect(Util.getElement('#foo')).toEqual(el) expect(Util.getElement('#fail')).toBeNull() expect(Util.getElement({})).toBeNull() expect(Util.getElement([])).toBeNull() expect(Util.getElement()).toBeNull() expect(Util.getElement(null)).toBeNull() expect(Util.getElement(fixtureEl.querySelectorAll('.test'))).toBeNull() const fakejQueryObject = { 0: el, jquery: 'foo' } expect(Util.getElement(fakejQueryObject)).toEqual(el) }) }) describe('isVisible', () => { it('should return false if the element is not defined', () => { expect(Util.isVisible(null)).toBeFalse() expect(Util.isVisible(undefined)).toBeFalse() }) it('should return false if the element provided is not a dom element', () => { expect(Util.isVisible({})).toBeFalse() }) it('should return false if the element is not visible with display none', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Util.isVisible(div)).toBeFalse() }) it('should return false if the element is not visible with visibility hidden', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') expect(Util.isVisible(div)).toBeFalse() }) it('should return false if an ancestor element is display none', () => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
', '
', '
' ].join('') const div = fixtureEl.querySelector('.content') expect(Util.isVisible(div)).toBeFalse() }) it('should return false if an ancestor element is visibility hidden', () => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
', '
', '
' ].join('') const div = fixtureEl.querySelector('.content') expect(Util.isVisible(div)).toBeFalse() }) it('should return true if an ancestor element is visibility hidden, but reverted', () => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
', '
', '
' ].join('') const div = fixtureEl.querySelector('.content') expect(Util.isVisible(div)).toBeTrue() }) it('should return true if the element is visible', () => { fixtureEl.innerHTML = [ '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') expect(Util.isVisible(div)).toBeTrue() }) it('should return false if the element is hidden, but not via display or visibility', () => { fixtureEl.innerHTML = [ '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') expect(Util.isVisible(div)).toBeFalse() }) it('should return true if its a closed details element', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('#element') expect(Util.isVisible(div)).toBeTrue() }) it('should return true if the element is visible inside an open details element', () => { fixtureEl.innerHTML = [ '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') expect(Util.isVisible(div)).toBeTrue() }) it('should return true if the element is a visible summary in a closed details element', () => { fixtureEl.innerHTML = [ '
', ' ', ' ', ' ', '
' ].join('') const element1 = fixtureEl.querySelector('#element-1') const element2 = fixtureEl.querySelector('#element-2') expect(Util.isVisible(element1)).toBeTrue() expect(Util.isVisible(element2)).toBeTrue() }) }) describe('isDisabled', () => { it('should return true if the element is not defined', () => { expect(Util.isDisabled(null)).toBeTrue() expect(Util.isDisabled(undefined)).toBeTrue() expect(Util.isDisabled()).toBeTrue() }) it('should return true if the element provided is not a dom element', () => { expect(Util.isDisabled({})).toBeTrue() expect(Util.isDisabled('test')).toBeTrue() }) it('should return true if the element has disabled attribute', () => { fixtureEl.innerHTML = [ '
', '
', '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') const div1 = fixtureEl.querySelector('#element1') const div2 = fixtureEl.querySelector('#element2') expect(Util.isDisabled(div)).toBeTrue() expect(Util.isDisabled(div1)).toBeTrue() expect(Util.isDisabled(div2)).toBeTrue() }) it('should return false if the element has disabled attribute with "false" value, or doesn\'t have attribute', () => { fixtureEl.innerHTML = [ '
', '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') const div1 = fixtureEl.querySelector('#element1') expect(Util.isDisabled(div)).toBeFalse() expect(Util.isDisabled(div1)).toBeFalse() }) it('should return false if the element is not disabled ', () => { fixtureEl.innerHTML = [ '
', ' ', ' ', ' ', '
' ].join('') const el = selector => fixtureEl.querySelector(selector) expect(Util.isDisabled(el('#button'))).toBeFalse() expect(Util.isDisabled(el('#select'))).toBeFalse() expect(Util.isDisabled(el('#input'))).toBeFalse() }) it('should return true if the element has disabled attribute', () => { fixtureEl.innerHTML = [ '
', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '
' ].join('') const el = selector => fixtureEl.querySelector(selector) expect(Util.isDisabled(el('#input'))).toBeTrue() expect(Util.isDisabled(el('#input1'))).toBeTrue() expect(Util.isDisabled(el('#button'))).toBeTrue() expect(Util.isDisabled(el('#button1'))).toBeTrue() expect(Util.isDisabled(el('#button2'))).toBeTrue() expect(Util.isDisabled(el('#input'))).toBeTrue() }) it('should return true if the element has class "disabled"', () => { fixtureEl.innerHTML = [ '
', '
', '
' ].join('') const div = fixtureEl.querySelector('#element') expect(Util.isDisabled(div)).toBeTrue() }) it('should return true if the element has class "disabled" but disabled attribute is false', () => { fixtureEl.innerHTML = [ '
', ' ', '
' ].join('') const div = fixtureEl.querySelector('#input') expect(Util.isDisabled(div)).toBeTrue() }) }) describe('findShadowRoot', () => { it('should return null if shadow dom is not available', () => { // Only for newer browsers if (!document.documentElement.attachShadow) { expect().nothing() return } fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') spyOn(document.documentElement, 'attachShadow').and.returnValue(null) expect(Util.findShadowRoot(div)).toBeNull() }) it('should return null when we do not find a shadow root', () => { // Only for newer browsers if (!document.documentElement.attachShadow) { expect().nothing() return } spyOn(document, 'getRootNode').and.returnValue(undefined) expect(Util.findShadowRoot(document)).toBeNull() }) it('should return the shadow root when found', () => { // Only for newer browsers if (!document.documentElement.attachShadow) { expect().nothing() return } fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const shadowRoot = div.attachShadow({ mode: 'open' }) expect(Util.findShadowRoot(shadowRoot)).toEqual(shadowRoot) shadowRoot.innerHTML = '' expect(Util.findShadowRoot(shadowRoot.firstChild)).toEqual(shadowRoot) }) }) describe('noop', () => { it('should be a function', () => { expect(Util.noop).toEqual(jasmine.any(Function)) }) }) describe('reflow', () => { it('should return element offset height to force the reflow', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const spy = spyOnProperty(div, 'offsetHeight') Util.reflow(div) expect(spy).toHaveBeenCalled() }) }) describe('getjQuery', () => { const fakejQuery = { trigger() {} } beforeEach(() => { Object.defineProperty(window, 'jQuery', { value: fakejQuery, writable: true }) }) afterEach(() => { window.jQuery = undefined }) it('should return jQuery object when present', () => { expect(Util.getjQuery()).toEqual(fakejQuery) }) it('should not return jQuery object when present if data-bs-no-jquery', () => { document.body.setAttribute('data-bs-no-jquery', '') expect(window.jQuery).toEqual(fakejQuery) expect(Util.getjQuery()).toBeNull() document.body.removeAttribute('data-bs-no-jquery') }) it('should not return jQuery if not present', () => { window.jQuery = undefined expect(Util.getjQuery()).toBeNull() }) }) describe('onDOMContentLoaded', () => { it('should execute callbacks when DOMContentLoaded is fired and should not add more than one listener', () => { const spy = jasmine.createSpy() const spy2 = jasmine.createSpy() const spyAdd = spyOn(document, 'addEventListener').and.callThrough() spyOnProperty(document, 'readyState').and.returnValue('loading') Util.onDOMContentLoaded(spy) Util.onDOMContentLoaded(spy2) document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true, cancelable: true })) expect(spy).toHaveBeenCalled() expect(spy2).toHaveBeenCalled() expect(spyAdd).toHaveBeenCalledTimes(1) }) it('should execute callback if readyState is not "loading"', () => { const spy = jasmine.createSpy() Util.onDOMContentLoaded(spy) expect(spy).toHaveBeenCalled() }) }) describe('defineJQueryPlugin', () => { const fakejQuery = { fn: {} } beforeEach(() => { Object.defineProperty(window, 'jQuery', { value: fakejQuery, writable: true }) }) afterEach(() => { window.jQuery = undefined }) it('should define a plugin on the jQuery instance', () => { const pluginMock = Util.noop pluginMock.NAME = 'test' pluginMock.jQueryInterface = Util.noop Util.defineJQueryPlugin(pluginMock) expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface) expect(fakejQuery.fn.test.Constructor).toEqual(pluginMock) expect(fakejQuery.fn.test.noConflict).toEqual(jasmine.any(Function)) }) }) describe('execute', () => { it('should execute if arg is function', () => { const spy = jasmine.createSpy('spy') Util.execute(spy) expect(spy).toHaveBeenCalled() }) it('should execute if arg is function & return the result', () => { const functionFoo = (num1, num2 = 10) => num1 + num2 const resultFoo = Util.execute(functionFoo, [undefined, 4, 5]) expect(resultFoo).toBe(9) const resultFoo1 = Util.execute(functionFoo, [undefined, 4]) expect(resultFoo1).toBe(14) const functionBar = () => 'foo' const resultBar = Util.execute(functionBar) expect(resultBar).toBe('foo') }) it('should not execute if arg is not function & return default argument', () => { const foo = 'bar' expect(Util.execute(foo)).toBe('bar') expect(Util.execute(foo, [], 4)).toBe(4) }) }) describe('executeAfterTransition', () => { it('should immediately execute a function when waitForTransition parameter is false', () => { const el = document.createElement('div') const callbackSpy = jasmine.createSpy('callback spy') const eventListenerSpy = spyOn(el, 'addEventListener') Util.executeAfterTransition(callbackSpy, el, false) expect(callbackSpy).toHaveBeenCalled() expect(eventListenerSpy).not.toHaveBeenCalled() }) it('should execute a function when a transitionend event is dispatched', () => { const el = document.createElement('div') const callbackSpy = jasmine.createSpy('callback spy') spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.05s', transitionDelay: '0s' }) Util.executeAfterTransition(callbackSpy, el) el.dispatchEvent(new TransitionEvent('transitionend')) expect(callbackSpy).toHaveBeenCalled() }) it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', () => { return new Promise(resolve => { const el = document.createElement('div') const callbackSpy = jasmine.createSpy('callback spy') spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.05s', transitionDelay: '0s' }) Util.executeAfterTransition(callbackSpy, el) setTimeout(() => { expect(callbackSpy).toHaveBeenCalled() resolve() }, 70) }) }) it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', () => { return new Promise(resolve => { const el = document.createElement('div') const callbackSpy = jasmine.createSpy('callback spy') spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.05s', transitionDelay: '0s' }) Util.executeAfterTransition(callbackSpy, el) setTimeout(() => { el.dispatchEvent(new TransitionEvent('transitionend')) }, 50) setTimeout(() => { expect(callbackSpy).toHaveBeenCalledTimes(1) resolve() }, 70) }) }) it('should not trigger a transitionend event if another transitionend event had already happened', () => { return new Promise(resolve => { const el = document.createElement('div') spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.05s', transitionDelay: '0s' }) Util.executeAfterTransition(noop, el) // simulate a event dispatched by the browser el.dispatchEvent(new TransitionEvent('transitionend')) const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() setTimeout(() => { // setTimeout should not have triggered another transitionend event. expect(dispatchSpy).not.toHaveBeenCalled() resolve() }, 70) }) }) it('should ignore transitionend events from nested elements', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
' ].join('') const outer = fixtureEl.querySelector('.outer') const nested = fixtureEl.querySelector('.nested') const callbackSpy = jasmine.createSpy('callback spy') spyOn(window, 'getComputedStyle').and.returnValue({ transitionDuration: '0.05s', transitionDelay: '0s' }) Util.executeAfterTransition(callbackSpy, outer) nested.dispatchEvent(new TransitionEvent('transitionend', { bubbles: true })) setTimeout(() => { expect(callbackSpy).not.toHaveBeenCalled() }, 20) setTimeout(() => { expect(callbackSpy).toHaveBeenCalled() resolve() }, 70) }) }) }) describe('getNextActiveElement', () => { it('should return first element if active not exists or not given and shouldGetNext is either true, or false with cycling being disabled', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a') expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a') expect(Util.getNextActiveElement(array, '', true, false)).toEqual('a') expect(Util.getNextActiveElement(array, 'g', true, false)).toEqual('a') expect(Util.getNextActiveElement(array, '', false, false)).toEqual('a') expect(Util.getNextActiveElement(array, 'g', false, false)).toEqual('a') }) it('should return last element if active not exists or not given and shouldGetNext is false but cycling is enabled', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, '', false, true)).toEqual('d') expect(Util.getNextActiveElement(array, 'g', false, true)).toEqual('d') }) it('should return next element or same if is last', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, 'a', true, true)).toEqual('b') expect(Util.getNextActiveElement(array, 'b', true, true)).toEqual('c') expect(Util.getNextActiveElement(array, 'd', true, false)).toEqual('d') }) it('should return next element or first, if is last and "isCycleAllowed = true"', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, 'c', true, true)).toEqual('d') expect(Util.getNextActiveElement(array, 'd', true, true)).toEqual('a') }) it('should return previous element or same if is first', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, 'b', false, true)).toEqual('a') expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c') expect(Util.getNextActiveElement(array, 'a', false, false)).toEqual('a') }) it('should return next element or first, if is last and "isCycleAllowed = true"', () => { const array = ['a', 'b', 'c', 'd'] expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c') expect(Util.getNextActiveElement(array, 'a', false, true)).toEqual('d') }) }) }) ================================================ FILE: js/tests/unit/util/sanitizer.spec.js ================================================ import { DefaultAllowlist, sanitizeHtml } from '../../../src/util/sanitizer.js' describe('Sanitizer', () => { describe('sanitizeHtml', () => { it('should return the same on empty string', () => { const empty = '' const result = sanitizeHtml(empty, DefaultAllowlist, null) expect(result).toEqual(empty) }) it('should retain tags with valid URLs', () => { const validUrls = [ '', 'http://abc', 'HTTP://abc', 'https://abc', 'HTTPS://abc', 'ftp://abc', 'FTP://abc', 'mailto:me@example.com', 'MAILTO:me@example.com', 'tel:123-123-1234', 'TEL:123-123-1234', 'sip:me@example.com', 'SIP:me@example.com', '#anchor', '/page1.md', 'http://JavaScript/my.js', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated. 'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', 'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', 'unknown-scheme:abc' ] for (const url of validUrls) { const template = [ '
', ` Click me`, ' Some content', '
' ].join('') const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result).toContain(`href="${url}"`) } }) it('should sanitize template by removing tags with XSS', () => { const invalidUrls = [ // eslint-disable-next-line no-script-url 'javascript:alert(7)', // eslint-disable-next-line no-script-url 'javascript:evil()', // eslint-disable-next-line no-script-url 'JavaScript:abc', ' javascript:abc', ' \n Java\n Script:abc', 'javascript:', 'javascript:', 'j avascript:', 'javascript:', 'javascript:', 'jav ascript:alert();', 'jav\u0000ascript:alert();' ] for (const url of invalidUrls) { const template = [ '
', ` Click me`, ' Some content', '
' ].join('') const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result).not.toContain(`href="${url}"`) } }) it('should sanitize template and work with multiple regex', () => { const template = [ '
', ' Click me', ' Some content', '
' ].join('') const myDefaultAllowList = DefaultAllowlist // With the default allow list let result = sanitizeHtml(template, myDefaultAllowList, null) // `data-foo` won't be present expect(result).not.toContain('data-foo="bar"') // Add the following regex too myDefaultAllowList['*'].push(/^data-foo/) result = sanitizeHtml(template, myDefaultAllowList, null) expect(result).not.toContain('href="javascript:alert(7)') // This is in the default list expect(result).toContain('aria-label="This is a link"') // This is in the default list expect(result).toContain('data-foo="bar"') // We explicitly allow this }) it('should allow aria attributes and safe attributes', () => { const template = [ '
', ' Some content', '
' ].join('') const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result).toContain('aria-pressed') expect(result).toContain('class="test"') }) it('should remove tags not in allowlist', () => { const template = [ '
', ' ', '
' ].join('') const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result).not.toContain(' ================================================ FILE: js/tests/visual/button.html ================================================ Button

Button Bootstrap Visual Test

For checkboxes and radio buttons, ensure that keyboard behavior is functioning correctly.

Navigate to the checkboxes with the keyboard (generally, using Tab / Shift + Tab), and ensure that Space toggles the currently focused checkbox. Click on one of the checkboxes using the mouse, ensure that focus was correctly set on the actual checkbox, and that Space toggles the checkbox again.

Navigate to the radio button group with the keyboard (generally, using Tab / Shift + Tab). If no radio button was initially set to be selected, the first/last radio button should receive focus (depending on whether you navigated "forward" to the group with Tab or "backwards" using Shift + Tab). If a radio button was already selected, navigating with the keyboard should set focus to that particular radio button. Only one radio button in a group should receive focus at any given time. Ensure that the selected radio button can be changed by using the and arrow keys. Click on one of the radio buttons with the mouse, ensure that focus was correctly set on the actual radio button, and that and change the selected radio button again.

================================================ FILE: js/tests/visual/carousel.html ================================================ Carousel

Carousel Bootstrap Visual Test

The transition duration should be around 2s. Also, the carousel shouldn't slide when its window/tab is hidden. Check the console log.

================================================ FILE: js/tests/visual/collapse.html ================================================ Collapse

Collapse Bootstrap Visual Test

================================================ FILE: js/tests/visual/dropdown.html ================================================ Dropdown

Dropdown Bootstrap Visual Test

Dropup split align end
Dropend split
Dropstart split
================================================ FILE: js/tests/visual/floating-label.html ================================================ Form
================================================ FILE: js/tests/visual/input.html ================================================ Form

Input Bootstrap Visual Test

No layout

Text
Email
Number
Date

Flex

Text
Email
Number
Date

Grid

Text
Email
Number
Date
================================================ FILE: js/tests/visual/modal.html ================================================ Modal

Modal Bootstrap Visual Test



(See Issue #18365)





================================================ FILE: js/tests/visual/popover.html ================================================ Popover

Popover Bootstrap Visual Test

================================================ FILE: js/tests/visual/scrollspy.html ================================================ Scrollspy

@fat

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


@mdo

Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard. Freegan beard aliqua cupidatat mcsweeney's vero. Cupidatat four loko nisi, ea helvetica nulla carles. Tattooed cosby sweater food truck, mcsweeney's quis non freegan vinyl. Lo-fi wes anderson +1 sartorial. Carles non aesthetic exercitation quis gentrify. Brooklyn adipisicing craft beer vice keytar deserunt.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


one

Occaecat commodo aliqua delectus. Fap craft beer deserunt skateboard ea. Lomo bicycle rights adipisicing banh mi, velit ea sunt next level locavore single-origin coffee in magna veniam. High life id vinyl, echo park consequat quis aliquip banh mi pitchfork. Vero VHS est adipisicing. Consectetur nisi DIY minim messenger bag. Cred ex in, sustainable delectus consectetur fanny pack iphone.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


two

In incididunt echo park, officia deserunt mcsweeney's proident master cleanse thundercats sapiente veniam. Excepteur VHS elit, proident shoreditch +1 biodiesel laborum craft beer. Single-origin coffee wayfarers irure four loko, cupidatat terry richardson master cleanse. Assumenda you probably haven't heard of them art party fanny pack, tattooed nulla cardigan tempor ad. Proident wolf nesciunt sartorial keffiyeh eu banh mi sustainable. Elit wolf voluptate, lo-fi ea portland before they sold out four loko. Locavore enim nostrud mlkshk brooklyn nesciunt.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


three

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Keytar twee blog, culpa messenger bag marfa whatever delectus food truck. Sapiente synth id assumenda. Locavore sed helvetica cliche irony, thundercats you probably haven't heard of them consequat hoodie gluten-free lo-fi fap aliquip. Labore elit placeat before they sold out, terry richardson proident brunch nesciunt quis cosby sweater pariatur keffiyeh ut helvetica artisan. Cardigan craft beer seitan readymade velit. VHS chambray laboris tempor veniam. Anim mollit minim commodo ullamco thundercats.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


Présentation

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.

Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.


Final section

Ad leggings keytar, brunch id art party dolor labore.

================================================ FILE: js/tests/visual/tab.html ================================================ Tab

Tab Bootstrap Visual Test

Tabs without fade

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Tabs with fade

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Tabs without fade (no initially active pane)

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Tabs with fade (no initially active pane)

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Tabs with nav and using links (with fade)

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater. Lomo wolf viral, mustache readymade thundercats keffiyeh craft beer marfa ethical. Wolf salvia freegan, sartorial keffiyeh echo park vegan.

Tabs with list-group (with fade)

================================================ FILE: js/tests/visual/toast.html ================================================ Toast

Toast Bootstrap Visual Test

================================================ FILE: js/tests/visual/tooltip.html ================================================ Tooltip

Tooltip Bootstrap Visual Test

Tight pants next level keffiyeh you probably haven't heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit american apparel have a terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, scenester farm-to-table banksy Austin freegan cred raw denim single-origin coffee viral.



Test Selector triggered tooltips
================================================ FILE: nuget/bootstrap.nuspec ================================================ bootstrap 5 Bootstrap CSS The Bootstrap Authors bootstrap The most popular front-end framework for developing responsive, mobile first projects on the web. https://blog.getbootstrap.com/ Bootstrap framework in CSS. Includes JavaScript. en-us https://getbootstrap.com/ bootstrap.png MIT Copyright 2011-2026 false css mobile-first responsive front-end framework web ================================================ FILE: nuget/bootstrap.sass.nuspec ================================================ bootstrap.sass 5 Bootstrap Sass The Bootstrap Authors bootstrap The most popular front-end framework for developing responsive, mobile first projects on the web. https://blog.getbootstrap.com/ Bootstrap framework in Sass. Includes JavaScript en-us https://getbootstrap.com/ bootstrap.png MIT Copyright 2011-2026 false css sass mobile-first responsive front-end framework web ================================================ FILE: package.js ================================================ // package metadata file for Meteor.js /* eslint-env meteor */ Package.describe({ name: 'twbs:bootstrap', // https://atmospherejs.com/twbs/bootstrap summary: 'The most popular front-end framework for developing responsive, mobile first projects on the web.', version: '5.3.8', git: 'https://github.com/twbs/bootstrap.git' }) Package.onUse(api => { api.versionsFrom('METEOR@1.0') api.addFiles([ 'dist/css/bootstrap.css', 'dist/js/bootstrap.js' ], 'client') }) ================================================ FILE: package.json ================================================ { "name": "bootstrap", "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", "version": "5.3.8", "config": { "version_short": "5.3" }, "keywords": [ "css", "sass", "mobile-first", "responsive", "front-end", "framework", "web" ], "homepage": "https://getbootstrap.com/", "author": "The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/twbs/bootstrap.git" }, "bugs": { "url": "https://github.com/twbs/bootstrap/issues" }, "funding": [ { "type": "github", "url": "https://github.com/sponsors/twbs" }, { "type": "opencollective", "url": "https://opencollective.com/bootstrap" } ], "main": "dist/js/bootstrap.js", "module": "dist/js/bootstrap.esm.js", "sass": "scss/bootstrap.scss", "style": "dist/css/bootstrap.css", "scripts": { "start": "npm-run-all --parallel watch docs-serve", "bundlewatch": "bundlewatch --config .bundlewatch.config.json", "css": "npm-run-all css-compile css-prefix css-rtl css-minify", "css-compile": "sass --style expanded --source-map --embed-sources --no-error-css scss/:dist/css/", "css-rtl": "cross-env NODE_ENV=RTL postcss --config build/postcss.config.mjs --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"", "css-lint": "npm-run-all --aggregate-output --continue-on-error --parallel css-lint-*", "css-lint-stylelint": "stylelint \"**/*.{css,scss}\" --cache --cache-location .cache/.stylelintcache", "css-lint-vars": "fusv scss/ site/src/scss/", "css-minify": "npm-run-all --aggregate-output --parallel css-minify-*", "css-minify-main": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"", "css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"", "css-prefix": "npm-run-all --aggregate-output --parallel css-prefix-*", "css-prefix-main": "postcss --config build/postcss.config.mjs --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"", "css-prefix-examples": "postcss --config build/postcss.config.mjs --replace \"site/src/assets/examples/**/*.css\"", "css-prefix-examples-rtl": "cross-env-shell NODE_ENV=RTL postcss --config build/postcss.config.mjs --dir \"site/src/assets/examples/\" --ext \".rtl.css\" --base \"site/src/assets/examples/\" \"site/src/assets/examples/{blog,carousel,dashboard,cheatsheet}/*.css\" \"!site/src/assets/examples/{blog,carousel,dashboard,cheatsheet}/*.rtl.css\"", "css-test": "jasmine --config=scss/tests/jasmine.js", "js": "npm-run-all js-compile js-minify", "js-compile": "npm-run-all --aggregate-output --parallel js-compile-*", "js-compile-standalone": "rollup --environment BUNDLE:false --config build/rollup.config.mjs --sourcemap", "js-compile-standalone-esm": "rollup --environment ESM:true,BUNDLE:false --config build/rollup.config.mjs --sourcemap", "js-compile-bundle": "rollup --environment BUNDLE:true --config build/rollup.config.mjs --sourcemap", "js-compile-plugins": "node build/build-plugins.mjs", "js-lint": "eslint --cache --cache-location .cache/.eslintcache --report-unused-disable-directives --ext .html,.js,.mjs,.md .", "js-minify": "npm-run-all --aggregate-output --parallel js-minify-*", "js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.js.map,includeSources,url=bootstrap.min.js.map\" --output dist/js/bootstrap.min.js dist/js/bootstrap.js", "js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.esm.js.map,includeSources,url=bootstrap.esm.min.js.map\" --output dist/js/bootstrap.esm.min.js dist/js/bootstrap.esm.js", "js-minify-bundle": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.bundle.js.map,includeSources,url=bootstrap.bundle.min.js.map\" --output dist/js/bootstrap.bundle.min.js dist/js/bootstrap.bundle.js", "js-test": "npm-run-all --aggregate-output --parallel js-test-karma js-test-jquery js-test-integration-*", "js-debug": "cross-env DEBUG=true npm run js-test-karma", "js-test-karma": "karma start js/tests/karma.conf.js", "js-test-integration-bundle": "rollup --config js/tests/integration/rollup.bundle.js", "js-test-integration-modularity": "rollup --config js/tests/integration/rollup.bundle-modularity.js", "js-test-cloud": "cross-env BROWSERSTACK=true npm run js-test-karma", "js-test-jquery": "cross-env JQUERY=true npm run js-test-karma", "lint": "npm-run-all --aggregate-output --continue-on-error --parallel js-lint css-lint lockfile-lint", "docs": "npm-run-all docs-build docs-lint", "docs-build": "npm run astro-build", "docs-compile": "npm run docs-build", "docs-vnu": "node build/vnu-jar.mjs", "docs-lint": "npm-run-all docs-prettier-check docs-vnu", "docs-prettier-check": "prettier --config site/.prettierrc.json -c --cache site", "docs-prettier-format": "prettier --config site/.prettierrc.json --write --cache site", "docs-serve": "npm run astro-dev", "docs-serve-only": "npx sirv-cli _site --port 9001", "lockfile-lint": "lockfile-lint --allowed-hosts npm --allowed-schemes https: --empty-hostname false --type npm --path package-lock.json", "update-deps": "ncu -u -x @docsearch/js,eslint,eslint-config-xo,eslint-plugin-unicorn,karma-browserstack-launcher,karma-rollup-preprocessor,sass,vnu-jar", "release": "npm-run-all dist release-sri docs-build release-zip*", "release-sri": "node build/generate-sri.mjs", "release-version": "node build/change-version.mjs", "release-zip": "cross-env-shell \"rm -rf bootstrap-$npm_package_version-dist bootstrap-$npm_package_version-dist.zip && cp -r dist/ bootstrap-$npm_package_version-dist && zip -qr9 bootstrap-$npm_package_version-dist.zip bootstrap-$npm_package_version-dist && rm -rf bootstrap-$npm_package_version-dist\"", "release-zip-examples": "node build/zip-examples.mjs", "dist": "npm-run-all --aggregate-output --parallel css js", "test": "npm-run-all lint dist js-test docs-build docs-lint", "netlify": "npm-run-all dist release-sri astro-build", "watch": "npm-run-all --parallel watch-*", "watch-css-main": "nodemon --watch scss/ --ext scss --exec \"npm-run-all css-lint css-compile css-prefix\"", "watch-css-dist": "nodemon --watch dist/css/ --ext css --ignore \"dist/css/*.rtl.*\" --exec \"npm run css-rtl\"", "watch-css-docs": "nodemon --watch site/src/scss/ --ext scss --exec \"npm run css-lint\"", "watch-css-test": "nodemon --watch scss/ --ext scss,js --exec \"npm run css-test\"", "watch-js-main": "nodemon --watch js/src/ --ext js --exec \"npm-run-all js-lint js-compile\"", "watch-js-docs": "nodemon --watch site/src/assets/ --ext js --exec \"npm run js-lint\"", "astro-dev": "astro dev --root site --port 9001", "astro-build": "astro build --root site && rm -rf _site && cp -r site/dist _site", "astro-preview": "astro preview --root site --port 9001" }, "peerDependencies": { "@popperjs/core": "^2.11.8" }, "devDependencies": { "@astrojs/check": "^0.9.6", "@astrojs/markdown-remark": "^6.3.10", "@astrojs/mdx": "^4.3.13", "@astrojs/prism": "^3.3.0", "@astrojs/sitemap": "^3.6.0", "@babel/cli": "^7.28.3", "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", "@docsearch/js": "^3.9.0", "@popperjs/core": "^2.11.8", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@stackblitz/sdk": "^1.11.0", "@types/js-yaml": "^4.0.9", "@types/mime": "^4.0.0", "@types/prismjs": "^1.26.6", "astro": "^5.16.6", "astro-auto-import": "^0.4.5", "astro-broken-links-checker": "^1.0.7", "autoprefixer": "^10.4.27", "bundlewatch": "^0.4.1", "clean-css-cli": "^5.6.3", "clipboard": "^2.0.11", "cross-env": "^10.1.0", "eslint": "8.57.1", "eslint-config-xo": "0.45.0", "eslint-plugin-html": "^8.1.3", "eslint-plugin-import": "^2.32.0", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-unicorn": "56.0.1", "find-unused-sass-variables": "^6.1.1", "github-slugger": "^2.0.0", "globby": "^16.0.0", "hammer-simulator": "0.0.1", "htmlparser2": "^12.0.0", "image-size": "^2.0.2", "ip": "^2.0.1", "jasmine": "^6.1.0", "jquery": "^3.7.1", "js-yaml": "^4.1.1", "karma": "^6.4.4", "karma-browserstack-launcher": "1.4.0", "karma-chrome-launcher": "^3.2.0", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-detect-browsers": "^2.3.3", "karma-firefox-launcher": "^2.1.3", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.2.0", "karma-rollup-preprocessor": "7.0.7", "lockfile-lint": "^5.0.0", "mime": "^4.1.0", "nodemon": "^3.1.14", "npm-run-all2": "^8.0.4", "postcss": "^8.5.8", "postcss-cli": "^11.0.1", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1", "rehype-autolink-headings": "^7.1.0", "remark": "^15.0.1", "remark-html": "^16.0.1", "rollup": "^4.54.0", "rollup-plugin-istanbul": "^5.0.0", "rtlcss": "^4.3.0", "sass": "1.78.0", "sass-true": "^10.1.0", "shelljs": "^0.10.0", "stylelint": "^16.26.1", "stylelint-config-twbs-bootstrap": "^16.1.0", "terser": "^5.44.1", "unist-util-visit": "^5.1.0", "vnu-jar": "25.11.25", "zod": "^4.3.6" }, "files": [ "dist/{css,js}/*.{css,js,map}", "js/{src,dist}/**/*.{js,map}", "js/index.{esm,umd}.js", "scss/**/*.scss", "!scss/tests/**" ], "jspm": { "registry": "npm", "main": "js/bootstrap", "directories": { "lib": "dist" }, "shim": { "js/bootstrap": { "deps": [ "@popperjs/core" ] } }, "dependencies": {}, "peerDependencies": { "@popperjs/core": "^2.11.8" } } } ================================================ FILE: scss/_accordion.scss ================================================ // // Base styles // .accordion { // scss-docs-start accordion-css-vars --#{$prefix}accordion-color: #{$accordion-color}; --#{$prefix}accordion-bg: #{$accordion-bg}; --#{$prefix}accordion-transition: #{$accordion-transition}; --#{$prefix}accordion-border-color: #{$accordion-border-color}; --#{$prefix}accordion-border-width: #{$accordion-border-width}; --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; --#{$prefix}accordion-btn-color: #{$accordion-button-color}; --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; // scss-docs-end accordion-css-vars } .accordion-button { position: relative; display: flex; align-items: center; width: 100%; padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); @include font-size($font-size-base); color: var(--#{$prefix}accordion-btn-color); text-align: left; // Reset button style background-color: var(--#{$prefix}accordion-btn-bg); border: 0; @include border-radius(0); overflow-anchor: none; @include transition(var(--#{$prefix}accordion-transition)); &:not(.collapsed) { color: var(--#{$prefix}accordion-active-color); background-color: var(--#{$prefix}accordion-active-bg); box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list &::after { background-image: var(--#{$prefix}accordion-btn-active-icon); transform: var(--#{$prefix}accordion-btn-icon-transform); } } // Accordion icon &::after { flex-shrink: 0; width: var(--#{$prefix}accordion-btn-icon-width); height: var(--#{$prefix}accordion-btn-icon-width); margin-left: auto; content: ""; background-image: var(--#{$prefix}accordion-btn-icon); background-repeat: no-repeat; background-size: var(--#{$prefix}accordion-btn-icon-width); @include transition(var(--#{$prefix}accordion-btn-icon-transition)); } &:hover { z-index: 2; } &:focus { z-index: 3; outline: 0; box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); } } .accordion-header { margin-bottom: 0; } .accordion-item { color: var(--#{$prefix}accordion-color); background-color: var(--#{$prefix}accordion-bg); border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); &:first-of-type { @include border-top-radius(var(--#{$prefix}accordion-border-radius)); > .accordion-header .accordion-button { @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); } } &:not(:first-of-type) { border-top: 0; } // Only set a border-radius on the last item if the accordion is collapsed &:last-of-type { @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); > .accordion-header .accordion-button { &.collapsed { @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); } } > .accordion-collapse { @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); } } } .accordion-body { padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); } // Flush accordion items // // Remove borders and border-radius to keep accordion items edge-to-edge. .accordion-flush { > .accordion-item { border-right: 0; border-left: 0; @include border-radius(0); &:first-child { border-top: 0; } &:last-child { border-bottom: 0; } // stylelint-disable selector-max-class > .accordion-collapse, > .accordion-header .accordion-button, > .accordion-header .accordion-button.collapsed { @include border-radius(0); } // stylelint-enable selector-max-class } } @if $enable-dark-mode { @include color-mode(dark) { .accordion-button::after { --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; } } } ================================================ FILE: scss/_alert.scss ================================================ // // Base styles // .alert { // scss-docs-start alert-css-vars --#{$prefix}alert-bg: transparent; --#{$prefix}alert-padding-x: #{$alert-padding-x}; --#{$prefix}alert-padding-y: #{$alert-padding-y}; --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; --#{$prefix}alert-color: inherit; --#{$prefix}alert-border-color: transparent; --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); --#{$prefix}alert-border-radius: #{$alert-border-radius}; --#{$prefix}alert-link-color: inherit; // scss-docs-end alert-css-vars position: relative; padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); margin-bottom: var(--#{$prefix}alert-margin-bottom); color: var(--#{$prefix}alert-color); background-color: var(--#{$prefix}alert-bg); border: var(--#{$prefix}alert-border); @include border-radius(var(--#{$prefix}alert-border-radius)); } // Headings for larger alerts .alert-heading { // Specified to prevent conflicts of changing $headings-color color: inherit; } // Provide class for links that match alerts .alert-link { font-weight: $alert-link-font-weight; color: var(--#{$prefix}alert-link-color); } // Dismissible alerts // // Expand the right padding and account for the close button's positioning. .alert-dismissible { padding-right: $alert-dismissible-padding-r; // Adjust close link position .btn-close { position: absolute; top: 0; right: 0; z-index: $stretched-link-z-index + 1; padding: $alert-padding-y * 1.25 $alert-padding-x; } } // scss-docs-start alert-modifiers // Generate contextual modifier classes for colorizing the alert @each $state in map-keys($theme-colors) { .alert-#{$state} { --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); } } // scss-docs-end alert-modifiers ================================================ FILE: scss/_badge.scss ================================================ // Base class // // Requires one of the contextual, color modifier classes for `color` and // `background-color`. .badge { // scss-docs-start badge-css-vars --#{$prefix}badge-padding-x: #{$badge-padding-x}; --#{$prefix}badge-padding-y: #{$badge-padding-y}; @include rfs($badge-font-size, --#{$prefix}badge-font-size); --#{$prefix}badge-font-weight: #{$badge-font-weight}; --#{$prefix}badge-color: #{$badge-color}; --#{$prefix}badge-border-radius: #{$badge-border-radius}; // scss-docs-end badge-css-vars display: inline-block; padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); @include font-size(var(--#{$prefix}badge-font-size)); font-weight: var(--#{$prefix}badge-font-weight); line-height: 1; color: var(--#{$prefix}badge-color); text-align: center; white-space: nowrap; vertical-align: baseline; @include border-radius(var(--#{$prefix}badge-border-radius)); @include gradient-bg(); // Empty badges collapse automatically &:empty { display: none; } } // Quick fix for badges in buttons .btn .badge { position: relative; top: -1px; } ================================================ FILE: scss/_breadcrumb.scss ================================================ .breadcrumb { // scss-docs-start breadcrumb-css-vars --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; // scss-docs-end breadcrumb-css-vars display: flex; flex-wrap: wrap; padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); @include font-size(var(--#{$prefix}breadcrumb-font-size)); list-style: none; background-color: var(--#{$prefix}breadcrumb-bg); @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); } .breadcrumb-item { // The separator between breadcrumbs (by default, a forward-slash: "/") + .breadcrumb-item { padding-left: var(--#{$prefix}breadcrumb-item-padding-x); &::before { float: left; // Suppress inline spacings and underlining of the separator padding-right: var(--#{$prefix}breadcrumb-item-padding-x); color: var(--#{$prefix}breadcrumb-divider-color); content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; } } &.active { color: var(--#{$prefix}breadcrumb-item-active-color); } } ================================================ FILE: scss/_button-group.scss ================================================ // Make the div behave like a button .btn-group, .btn-group-vertical { position: relative; display: inline-flex; vertical-align: middle; // match .btn alignment given font-size hack above > .btn { position: relative; flex: 1 1 auto; } // Bring the hover, focused, and "active" buttons to the front to overlay // the borders properly > .btn-check:checked + .btn, > .btn-check:focus + .btn, > .btn:hover, > .btn:focus, > .btn:active, > .btn.active { z-index: 1; } } // Optional: Group multiple button groups together for a toolbar .btn-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-start; .input-group { width: auto; } } .btn-group { @include border-radius($btn-border-radius); // Prevent double borders when buttons are next to each other > :not(.btn-check:first-child) + .btn, > .btn-group:not(:first-child) { margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list } // Reset rounded corners > .btn:not(:last-child):not(.dropdown-toggle), > .btn.dropdown-toggle-split:first-child, > .btn-group:not(:last-child) > .btn { @include border-end-radius(0); } // The left radius should be 0 if the button is: // - the "third or more" child // - the second child and the previous element isn't `.btn-check` (making it the first child visually) // - part of a btn-group which isn't the first child > .btn:nth-child(n + 3), > :not(.btn-check) + .btn, > .btn-group:not(:first-child) > .btn { @include border-start-radius(0); } } // Sizing // // Remix the default button sizing classes into new ones for easier manipulation. .btn-group-sm > .btn { @extend .btn-sm; } .btn-group-lg > .btn { @extend .btn-lg; } // // Split button dropdowns // .dropdown-toggle-split { padding-right: $btn-padding-x * .75; padding-left: $btn-padding-x * .75; &::after, .dropup &::after, .dropend &::after { margin-left: 0; } .dropstart &::before { margin-right: 0; } } .btn-sm + .dropdown-toggle-split { padding-right: $btn-padding-x-sm * .75; padding-left: $btn-padding-x-sm * .75; } .btn-lg + .dropdown-toggle-split { padding-right: $btn-padding-x-lg * .75; padding-left: $btn-padding-x-lg * .75; } // The clickable button for toggling the menu // Set the same inset shadow as the :active state .btn-group.show .dropdown-toggle { @include box-shadow($btn-active-box-shadow); // Show no shadow for `.btn-link` since it has no other button styles. &.btn-link { @include box-shadow(none); } } // // Vertical button groups // .btn-group-vertical { flex-direction: column; align-items: flex-start; justify-content: center; > .btn, > .btn-group { width: 100%; } > .btn:not(:first-child), > .btn-group:not(:first-child) { margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list } // Reset rounded corners > .btn:not(:last-child):not(.dropdown-toggle), > .btn-group:not(:last-child) > .btn { @include border-bottom-radius(0); } // The top radius should be 0 if the button is: // - the "third or more" child // - the second child and the previous element isn't `.btn-check` (making it the first child visually) // - part of a btn-group which isn't the first child > .btn:nth-child(n + 3), > :not(.btn-check) + .btn, > .btn-group:not(:first-child) > .btn { @include border-top-radius(0); } } ================================================ FILE: scss/_buttons.scss ================================================ // // Base styles // .btn { // scss-docs-start btn-css-vars --#{$prefix}btn-padding-x: #{$btn-padding-x}; --#{$prefix}btn-padding-y: #{$btn-padding-y}; --#{$prefix}btn-font-family: #{$btn-font-family}; @include rfs($btn-font-size, --#{$prefix}btn-font-size); --#{$prefix}btn-font-weight: #{$btn-font-weight}; --#{$prefix}btn-line-height: #{$btn-line-height}; --#{$prefix}btn-color: #{$btn-color}; --#{$prefix}btn-bg: transparent; --#{$prefix}btn-border-width: #{$btn-border-width}; --#{$prefix}btn-border-color: transparent; --#{$prefix}btn-border-radius: #{$btn-border-radius}; --#{$prefix}btn-hover-border-color: transparent; --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); // scss-docs-end btn-css-vars display: inline-block; padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); font-family: var(--#{$prefix}btn-font-family); @include font-size(var(--#{$prefix}btn-font-size)); font-weight: var(--#{$prefix}btn-font-weight); line-height: var(--#{$prefix}btn-line-height); color: var(--#{$prefix}btn-color); text-align: center; text-decoration: if($link-decoration == none, null, none); white-space: $btn-white-space; vertical-align: middle; cursor: if($enable-button-pointers, pointer, null); user-select: none; border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); @include border-radius(var(--#{$prefix}btn-border-radius)); @include gradient-bg(var(--#{$prefix}btn-bg)); @include box-shadow(var(--#{$prefix}btn-box-shadow)); @include transition($btn-transition); &:hover { color: var(--#{$prefix}btn-hover-color); text-decoration: if($link-hover-decoration == underline, none, null); background-color: var(--#{$prefix}btn-hover-bg); border-color: var(--#{$prefix}btn-hover-border-color); } .btn-check + &:hover { // override for the checkbox/radio buttons color: var(--#{$prefix}btn-color); background-color: var(--#{$prefix}btn-bg); border-color: var(--#{$prefix}btn-border-color); } &:focus-visible { color: var(--#{$prefix}btn-hover-color); @include gradient-bg(var(--#{$prefix}btn-hover-bg)); border-color: var(--#{$prefix}btn-hover-border-color); outline: 0; // Avoid using mixin so we can pass custom focus shadow properly @if $enable-shadows { box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); } @else { box-shadow: var(--#{$prefix}btn-focus-box-shadow); } } .btn-check:focus-visible + & { border-color: var(--#{$prefix}btn-hover-border-color); outline: 0; // Avoid using mixin so we can pass custom focus shadow properly @if $enable-shadows { box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); } @else { box-shadow: var(--#{$prefix}btn-focus-box-shadow); } } .btn-check:checked + &, :not(.btn-check) + &:active, &:first-child:active, &.active, &.show { color: var(--#{$prefix}btn-active-color); background-color: var(--#{$prefix}btn-active-bg); // Remove CSS gradients if they're enabled background-image: if($enable-gradients, none, null); border-color: var(--#{$prefix}btn-active-border-color); @include box-shadow(var(--#{$prefix}btn-active-shadow)); &:focus-visible { // Avoid using mixin so we can pass custom focus shadow properly @if $enable-shadows { box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); } @else { box-shadow: var(--#{$prefix}btn-focus-box-shadow); } } } .btn-check:checked:focus-visible + & { // Avoid using mixin so we can pass custom focus shadow properly @if $enable-shadows { box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); } @else { box-shadow: var(--#{$prefix}btn-focus-box-shadow); } } &:disabled, &.disabled, fieldset:disabled & { color: var(--#{$prefix}btn-disabled-color); pointer-events: none; background-color: var(--#{$prefix}btn-disabled-bg); background-image: if($enable-gradients, none, null); border-color: var(--#{$prefix}btn-disabled-border-color); opacity: var(--#{$prefix}btn-disabled-opacity); @include box-shadow(none); } } // // Alternate buttons // // scss-docs-start btn-variant-loops @each $color, $value in $theme-colors { .btn-#{$color} { @if $color == "light" { @include button-variant( $value, $value, $hover-background: shade-color($value, $btn-hover-bg-shade-amount), $hover-border: shade-color($value, $btn-hover-border-shade-amount), $active-background: shade-color($value, $btn-active-bg-shade-amount), $active-border: shade-color($value, $btn-active-border-shade-amount) ); } @else if $color == "dark" { @include button-variant( $value, $value, $hover-background: tint-color($value, $btn-hover-bg-tint-amount), $hover-border: tint-color($value, $btn-hover-border-tint-amount), $active-background: tint-color($value, $btn-active-bg-tint-amount), $active-border: tint-color($value, $btn-active-border-tint-amount) ); } @else { @include button-variant($value, $value); } } } @each $color, $value in $theme-colors { .btn-outline-#{$color} { @include button-outline-variant($value); } } // scss-docs-end btn-variant-loops // // Link buttons // // Make a button look and behave like a link .btn-link { --#{$prefix}btn-font-weight: #{$font-weight-normal}; --#{$prefix}btn-color: #{$btn-link-color}; --#{$prefix}btn-bg: transparent; --#{$prefix}btn-border-color: transparent; --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; --#{$prefix}btn-hover-border-color: transparent; --#{$prefix}btn-active-color: #{$btn-link-hover-color}; --#{$prefix}btn-active-border-color: transparent; --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; --#{$prefix}btn-disabled-border-color: transparent; --#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows --#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb}; text-decoration: $link-decoration; @if $enable-gradients { background-image: none; } &:hover, &:focus-visible { text-decoration: $link-hover-decoration; } &:focus-visible { color: var(--#{$prefix}btn-color); } &:hover { color: var(--#{$prefix}btn-hover-color); } // No need for an active state here } // // Button Sizes // .btn-lg { @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); } .btn-sm { @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); } ================================================ FILE: scss/_card.scss ================================================ // // Base styles // .card { // scss-docs-start card-css-vars --#{$prefix}card-spacer-y: #{$card-spacer-y}; --#{$prefix}card-spacer-x: #{$card-spacer-x}; --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; --#{$prefix}card-title-color: #{$card-title-color}; --#{$prefix}card-subtitle-color: #{$card-subtitle-color}; --#{$prefix}card-border-width: #{$card-border-width}; --#{$prefix}card-border-color: #{$card-border-color}; --#{$prefix}card-border-radius: #{$card-border-radius}; --#{$prefix}card-box-shadow: #{$card-box-shadow}; --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; --#{$prefix}card-cap-bg: #{$card-cap-bg}; --#{$prefix}card-cap-color: #{$card-cap-color}; --#{$prefix}card-height: #{$card-height}; --#{$prefix}card-color: #{$card-color}; --#{$prefix}card-bg: #{$card-bg}; --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; --#{$prefix}card-group-margin: #{$card-group-margin}; // scss-docs-end card-css-vars position: relative; display: flex; flex-direction: column; min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 height: var(--#{$prefix}card-height); color: var(--#{$prefix}body-color); word-wrap: break-word; background-color: var(--#{$prefix}card-bg); background-clip: border-box; border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); @include border-radius(var(--#{$prefix}card-border-radius)); @include box-shadow(var(--#{$prefix}card-box-shadow)); > hr { margin-right: 0; margin-left: 0; } > .list-group { border-top: inherit; border-bottom: inherit; &:first-child { border-top-width: 0; @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); } &:last-child { border-bottom-width: 0; @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); } } // Due to specificity of the above selector (`.card > .list-group`), we must // use a child selector here to prevent double borders. > .card-header + .list-group, > .list-group + .card-footer { border-top: 0; } } .card-body { // Enable `flex-grow: 1` for decks and groups so that card blocks take up // as much space as possible, ensuring footers are aligned to the bottom. flex: 1 1 auto; padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); color: var(--#{$prefix}card-color); } .card-title { margin-bottom: var(--#{$prefix}card-title-spacer-y); color: var(--#{$prefix}card-title-color); } .card-subtitle { margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list margin-bottom: 0; color: var(--#{$prefix}card-subtitle-color); } .card-text:last-child { margin-bottom: 0; } .card-link { &:hover { text-decoration: if($link-hover-decoration == underline, none, null); } + .card-link { margin-left: var(--#{$prefix}card-spacer-x); } } // // Optional textual caps // .card-header { padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); margin-bottom: 0; // Removes the default margin-bottom of color: var(--#{$prefix}card-cap-color); background-color: var(--#{$prefix}card-cap-bg); border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); &:first-child { @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); } } .card-footer { padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); color: var(--#{$prefix}card-cap-color); background-color: var(--#{$prefix}card-cap-bg); border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); &:last-child { @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); } } // // Header navs // .card-header-tabs { margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list border-bottom: 0; .nav-link.active { background-color: var(--#{$prefix}card-bg); border-bottom-color: var(--#{$prefix}card-bg); } } .card-header-pills { margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list } // Card image .card-img-overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: var(--#{$prefix}card-img-overlay-padding); @include border-radius(var(--#{$prefix}card-inner-border-radius)); } .card-img, .card-img-top, .card-img-bottom { width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch } .card-img, .card-img-top { @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); } .card-img, .card-img-bottom { @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); } // // Card groups // .card-group { // The child selector allows nested `.card` within `.card-group` // to display properly. > .card { margin-bottom: var(--#{$prefix}card-group-margin); } @include media-breakpoint-up(sm) { display: flex; flex-flow: row wrap; // The child selector allows nested `.card` within `.card-group` // to display properly. > .card { flex: 1 0 0; margin-bottom: 0; + .card { margin-left: 0; border-left: 0; } // Handle rounded corners @if $enable-rounded { &:not(:last-child) { @include border-end-radius(0); > .card-img-top, > .card-header { // stylelint-disable-next-line property-disallowed-list border-top-right-radius: 0; } > .card-img-bottom, > .card-footer { // stylelint-disable-next-line property-disallowed-list border-bottom-right-radius: 0; } } &:not(:first-child) { @include border-start-radius(0); > .card-img-top, > .card-header { // stylelint-disable-next-line property-disallowed-list border-top-left-radius: 0; } > .card-img-bottom, > .card-footer { // stylelint-disable-next-line property-disallowed-list border-bottom-left-radius: 0; } } } } } } ================================================ FILE: scss/_carousel.scss ================================================ // Notes on the classes: // // 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) // even when their scroll action started on a carousel, but for compatibility (with Firefox) // we're preventing all actions instead // 2. The .carousel-item-start and .carousel-item-end is used to indicate where // the active slide is heading. // 3. .active.carousel-item is the current slide. // 4. .active.carousel-item-start and .active.carousel-item-end is the current // slide in its in-transition state. Only one of these occurs at a time. // 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end // is the upcoming slide in transition. .carousel { position: relative; } .carousel.pointer-event { touch-action: pan-y; } .carousel-inner { position: relative; width: 100%; overflow: hidden; @include clearfix(); } .carousel-item { position: relative; display: none; float: left; width: 100%; margin-right: -100%; backface-visibility: hidden; @include transition($carousel-transition); } .carousel-item.active, .carousel-item-next, .carousel-item-prev { display: block; } .carousel-item-next:not(.carousel-item-start), .active.carousel-item-end { transform: translateX(100%); } .carousel-item-prev:not(.carousel-item-end), .active.carousel-item-start { transform: translateX(-100%); } // // Alternate transitions // .carousel-fade { .carousel-item { opacity: 0; transition-property: opacity; transform: none; } .carousel-item.active, .carousel-item-next.carousel-item-start, .carousel-item-prev.carousel-item-end { z-index: 1; opacity: 1; } .active.carousel-item-start, .active.carousel-item-end { z-index: 0; opacity: 0; @include transition(opacity 0s $carousel-transition-duration); } } // // Left/right controls for nav // .carousel-control-prev, .carousel-control-next { position: absolute; top: 0; bottom: 0; z-index: 1; // Use flex for alignment (1-3) display: flex; // 1. allow flex styles align-items: center; // 2. vertically center contents justify-content: center; // 3. horizontally center contents width: $carousel-control-width; padding: 0; color: $carousel-control-color; text-align: center; background: none; filter: var(--#{$prefix}carousel-control-icon-filter); border: 0; opacity: $carousel-control-opacity; @include transition($carousel-control-transition); // Hover/focus state &:hover, &:focus { color: $carousel-control-color; text-decoration: none; outline: 0; opacity: $carousel-control-hover-opacity; } } .carousel-control-prev { left: 0; background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); } .carousel-control-next { right: 0; background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); } // Icons for within .carousel-control-prev-icon, .carousel-control-next-icon { display: inline-block; width: $carousel-control-icon-width; height: $carousel-control-icon-width; background-repeat: no-repeat; background-position: 50%; background-size: 100% 100%; } .carousel-control-prev-icon { background-image: escape-svg($carousel-control-prev-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-next-icon-bg) + "*/"}; } .carousel-control-next-icon { background-image: escape-svg($carousel-control-next-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-prev-icon-bg) + "*/"}; } // Optional indicator pips/controls // // Add a container (such as a list) with the following class and add an item (ideally a focusable control, // like a button) with data-bs-target for each slide your carousel holds. .carousel-indicators { position: absolute; right: 0; bottom: 0; left: 0; z-index: 2; display: flex; justify-content: center; padding: 0; // Use the .carousel-control's width as margin so we don't overlay those margin-right: $carousel-control-width; margin-bottom: 1rem; margin-left: $carousel-control-width; [data-bs-target] { box-sizing: content-box; flex: 0 1 auto; width: $carousel-indicator-width; height: $carousel-indicator-height; padding: 0; margin-right: $carousel-indicator-spacer; margin-left: $carousel-indicator-spacer; text-indent: -999px; cursor: pointer; background-color: var(--#{$prefix}carousel-indicator-active-bg); background-clip: padding-box; border: 0; // Use transparent borders to increase the hit area by 10px on top and bottom. border-top: $carousel-indicator-hit-area-height solid transparent; border-bottom: $carousel-indicator-hit-area-height solid transparent; opacity: $carousel-indicator-opacity; @include transition($carousel-indicator-transition); } .active { opacity: $carousel-indicator-active-opacity; } } // Optional captions // // .carousel-caption { position: absolute; right: (100% - $carousel-caption-width) * .5; bottom: $carousel-caption-spacer; left: (100% - $carousel-caption-width) * .5; padding-top: $carousel-caption-padding-y; padding-bottom: $carousel-caption-padding-y; color: var(--#{$prefix}carousel-caption-color); text-align: center; } // Dark mode carousel @mixin carousel-dark() { --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; } .carousel-dark { @include carousel-dark(); } :root, [data-bs-theme="light"] { --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; } @if $enable-dark-mode { @include color-mode(dark, true) { @include carousel-dark(); } } ================================================ FILE: scss/_close.scss ================================================ // Transparent background and border properties included for button version. // iOS requires the button element instead of an anchor tag. // If you want the anchor version, it requires `href="#"`. // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile .btn-close { // scss-docs-start close-css-vars --#{$prefix}btn-close-color: #{$btn-close-color}; --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; // scss-docs-end close-css-vars box-sizing: content-box; width: $btn-close-width; height: $btn-close-height; padding: $btn-close-padding-y $btn-close-padding-x; color: var(--#{$prefix}btn-close-color); background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements filter: var(--#{$prefix}btn-close-filter); border: 0; // for button elements @include border-radius(); opacity: var(--#{$prefix}btn-close-opacity); // Override 's hover style &:hover { color: var(--#{$prefix}btn-close-color); text-decoration: none; opacity: var(--#{$prefix}btn-close-hover-opacity); } &:focus { outline: 0; box-shadow: var(--#{$prefix}btn-close-focus-shadow); opacity: var(--#{$prefix}btn-close-focus-opacity); } &:disabled, &.disabled { pointer-events: none; user-select: none; opacity: var(--#{$prefix}btn-close-disabled-opacity); } } @mixin btn-close-white() { --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; } .btn-close-white { @include btn-close-white(); } :root, [data-bs-theme="light"] { --#{$prefix}btn-close-filter: #{$btn-close-filter}; } @if $enable-dark-mode { @include color-mode(dark, true) { @include btn-close-white(); } } ================================================ FILE: scss/_containers.scss ================================================ // Container widths // // Set the container width, and override it for fixed navbars in media queries. @if $enable-container-classes { // Single container class with breakpoint max-widths .container, // 100% wide container at all breakpoints .container-fluid { @include make-container(); } // Responsive containers that are 100% wide until a breakpoint @each $breakpoint, $container-max-width in $container-max-widths { .container-#{$breakpoint} { @extend .container-fluid; } @include media-breakpoint-up($breakpoint, $grid-breakpoints) { %responsive-container-#{$breakpoint} { max-width: $container-max-width; } // Extend each breakpoint which is smaller or equal to the current breakpoint $extend-breakpoint: true; @each $name, $width in $grid-breakpoints { @if ($extend-breakpoint) { .container#{breakpoint-infix($name, $grid-breakpoints)} { @extend %responsive-container-#{$breakpoint}; } // Once the current breakpoint is reached, stop extending @if ($breakpoint == $name) { $extend-breakpoint: false; } } } } } } ================================================ FILE: scss/_dropdown.scss ================================================ // The dropdown wrapper (`
`) .dropup, .dropend, .dropdown, .dropstart, .dropup-center, .dropdown-center { position: relative; } .dropdown-toggle { white-space: nowrap; // Generate the caret automatically @include caret(); } // The dropdown menu .dropdown-menu { // scss-docs-start dropdown-css-vars --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); --#{$prefix}dropdown-color: #{$dropdown-color}; --#{$prefix}dropdown-bg: #{$dropdown-bg}; --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; // scss-docs-end dropdown-css-vars position: absolute; z-index: var(--#{$prefix}dropdown-zindex); display: none; // none by default, but block on "open" of the menu min-width: var(--#{$prefix}dropdown-min-width); padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); margin: 0; // Override default margin of ul @include font-size(var(--#{$prefix}dropdown-font-size)); color: var(--#{$prefix}dropdown-color); text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) list-style: none; background-color: var(--#{$prefix}dropdown-bg); background-clip: padding-box; border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); @include border-radius(var(--#{$prefix}dropdown-border-radius)); @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); &[data-bs-popper] { top: 100%; left: 0; margin-top: var(--#{$prefix}dropdown-spacer); } @if $dropdown-padding-y == 0 { > .dropdown-item:first-child, > li:first-child .dropdown-item { @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); } > .dropdown-item:last-child, > li:last-child .dropdown-item { @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); } } } // scss-docs-start responsive-breakpoints // We deliberately hardcode the `bs-` prefix because we check // this custom property in JS to determine Popper's positioning @each $breakpoint in map-keys($grid-breakpoints) { @include media-breakpoint-up($breakpoint) { $infix: breakpoint-infix($breakpoint, $grid-breakpoints); .dropdown-menu#{$infix}-start { --bs-position: start; &[data-bs-popper] { right: auto; left: 0; } } .dropdown-menu#{$infix}-end { --bs-position: end; &[data-bs-popper] { right: 0; left: auto; } } } } // scss-docs-end responsive-breakpoints // Allow for dropdowns to go bottom up (aka, dropup-menu) // Just add .dropup after the standard .dropdown class and you're set. .dropup { .dropdown-menu[data-bs-popper] { top: auto; bottom: 100%; margin-top: 0; margin-bottom: var(--#{$prefix}dropdown-spacer); } .dropdown-toggle { @include caret(up); } } .dropend { .dropdown-menu[data-bs-popper] { top: 0; right: auto; left: 100%; margin-top: 0; margin-left: var(--#{$prefix}dropdown-spacer); } .dropdown-toggle { @include caret(end); &::after { vertical-align: 0; } } } .dropstart { .dropdown-menu[data-bs-popper] { top: 0; right: 100%; left: auto; margin-top: 0; margin-right: var(--#{$prefix}dropdown-spacer); } .dropdown-toggle { @include caret(start); &::before { vertical-align: 0; } } } // Dividers (basically an `
`) within the dropdown .dropdown-divider { height: 0; margin: var(--#{$prefix}dropdown-divider-margin-y) 0; overflow: hidden; border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); opacity: 1; // Revisit in v6 to de-dupe styles that conflict with
element } // Links, buttons, and more within the dropdown menu // // `

Album example

Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.

Main call to action Secondary action

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

9 mins
================================================ FILE: site/src/assets/examples/album-rtl/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' import Placeholder from '@shortcodes/Placeholder.astro' export const title = 'مثال الألبوم' export const direction = 'rtl' ---

مثال الألبوم

وصف قصير حول الألبوم أدناه (محتوياته ، ومنشؤه ، وما إلى ذلك). اجعله قصير ولطيف، ولكن ليست قصير جدًا حتى لا يتخطى الناس هذا الألبوم تمامًا.

الدعوة الرئيسية للعمل عمل ثانوي

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق

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

9 دقائق
================================================ FILE: site/src/assets/examples/badges/badges.css ================================================ .badge > a { color: inherit; } ================================================ FILE: site/src/assets/examples/badges/index.astro ================================================ --- export const title = 'Badges' export const extra_css = ['badges.css'] ---
Primary Secondary Success Danger Warning Info Light Dark
Primary Secondary Success Danger Warning Info Light Dark
Primary Secondary Success Danger Warning Info Light Dark
Primary Secondary Success Danger Warning Info Light Dark
Primary 1 Primary 2 Primary 3
Primary Secondary Success Danger Warning Info Light Dark
================================================ FILE: site/src/assets/examples/blog/blog.css ================================================ /* stylelint-disable @stylistic/selector-list-comma-newline-after */ .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*/; } .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 posts */ .blog-post { margin-bottom: 4rem; } .blog-post-meta { margin-bottom: 1.25rem; color: #727272; } ================================================ FILE: site/src/assets/examples/blog/blog.rtl.css ================================================ /* stylelint-disable @stylistic/selector-list-comma-newline-after */ .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; } .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 posts */ .blog-post { margin-bottom: 4rem; } .blog-post-meta { margin-bottom: 1.25rem; color: #727272; } ================================================ FILE: site/src/assets/examples/blog/index.astro ================================================ --- export const title = 'Blog Template' export const extra_css = ['https://fonts.googleapis.com/css?family=Playfair+Display:700,900&display=swap', 'blog.css'] import Placeholder from "@shortcodes/Placeholder.astro" ---

Title of a longer featured blog post

Multiple lines of text that form the lede, informing new readers quickly and efficiently about what’s most interesting in this post’s contents.

Continue reading...

World

Featured post

Nov 12

This is a wider card with supporting text below as a natural lead-in to additional content.

Continue reading
Design

Post title

Nov 11

This is a wider card with supporting text below as a natural lead-in to additional content.

Continue reading

From the Firehose

This blog post shows a few different types of content that’s supported and styled with Bootstrap. Basic typography, lists, tables, images, code, and more are all supported as expected.


This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Blockquotes

This is an example blockquote in action:

Quoted text goes here.

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Example lists

This is some additional paragraph placeholder content. It's a slightly shorter version of the other highly repetitive body text used throughout. This is an example unordered list:

  • First list item
  • Second list item with a longer description
  • Third list item to close it out

And this is an ordered list:

  1. First list item
  2. Second list item with a longer description
  3. Third list item to close it out

And this is a definition list:

HyperText Markup Language (HTML)
The language used to describe and define the content of a Web page
Cascading Style Sheets (CSS)
Used to describe the appearance of Web content
JavaScript (JS)
The programming language used to build advanced Web sites and applications

Inline HTML elements

HTML defines a long list of available inline tags, a complete list of which can be found on the Mozilla Developer Network.

  • To bold text, use <strong>.
  • To italicize text, use <em>.
  • Abbreviations, like HTML should use <abbr>, with an optional title attribute for the full phrase.
  • Citations, like — Mark Otto, should use <cite>.
  • Deleted text should use <del> and inserted text should use <ins>.
  • Superscript text uses <sup> and subscript text uses <sub>.

Most of these elements are styled by browsers with few modifications on our part.

Heading

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Sub-heading

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Example code block

This is some additional paragraph placeholder content. It's a slightly shorter version of the other highly repetitive body text used throughout.

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Longer quote goes here, maybe with some emphasized text in the middle of it.

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

Example table

And don't forget about tables in these posts:

Name Upvotes Downvotes
Alice 10 11
Bob 4 3
Charlie 7 9
Totals 21 23

This is some additional paragraph placeholder content. It's a slightly shorter version of the other highly repetitive body text used throughout.

This is some additional paragraph placeholder content. It has been written to fill the available space and show how a longer snippet of text affects the surrounding content. We'll repeat it often to keep the demonstration flowing, so be on the lookout for this exact same string of text.

  • First list item
  • Second list item with a longer description
  • Third list item to close it out

This is some additional paragraph placeholder content. It's a slightly shorter version of the other highly repetitive body text used throughout.

================================================ FILE: site/src/assets/examples/blog-rtl/index.astro ================================================ --- export const title = 'قالب المدونة' export const direction = 'rtl' export const extra_css = ['https://fonts.googleapis.com/css?family=Amiri:wght@400;700&display=swap', '../blog/blog.rtl.css'] import Placeholder from "@shortcodes/Placeholder.astro" ---

عنوان تدوينة مميزة أطول

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

أكمل القراءة...

العالم

مشاركة مميزة

نوفمبر 12

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

أكمل القراءة
التصميم

عنوان الوظيفة

نوفمبر 11

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

أكمل القراءة

من Firehose

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


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

تشغل الزراعة مساحات كبيرة من اليابسة، وتستهلك كميات هائلة من المياه، كما أن إنتاج الغذاء بواسطة الزراعة يسهم بنسبة عالية من انبعاثات غازات الاحتباس الحراري العالمية

تشغل الزراعة مساحات كبيرة من اليابسة، وتستهلك كميات هائلة من المياه. كما أن إنتاج الغذاء بواسطة الزراعة يسهم بنسبة عالية من انبعاثات غازات الاحتباس الحراري العالمية، وللمقارنة فإن هذه النسبة من الانبعاثات هي أكبر مما ينتجه قطاع النقل بكل ما فيه من سيارات وشاحنات وطائرات وقطارات.

عنوان

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

عنوان فرعي

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

Example code block

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

عنوان فرعي

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

يتكوَّن الغذاء البشري من ثلاثة أنواع رئيسة، هي:

  • البروتينات
  • الكربوهيدرات
  • الدهون

وتتكوَّن البروتينات من الأحماض الأمينية، وهي مجموعة من المركبات العضوية يبلغ عددها في جسم الإنسان عشرين حمضاً أمينياً، من بينها تسعة أساسية يحصل عليها الجسم من الغذاء. وتتكوَّن الأحماض الأمينية بشكل أساس من:

  1. الكربون
  2. الهيدروجين
  3. الأكسجين
  4. النيتروجين

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

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

تم تصنيع اللحوم بأنواع عديدة

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

وتم تصنيع اللحوم بأنواع عديدة بما فيها الدواجن والأبقار والمأكولات البحرية، من دون حصول انبعاثات كربونية، على عكس تربية الأبقار التي تسهم في انبعاث غاز الميثان أحد غازات الاحتباس الحراري.

كما أن الشركة الفنلندية (Solar Foods) طوَّرت تقنية لإنتاج البروتين من الهواء، حيث تبدأ العملية بتقسيم الماء إلى مكوناته الهيدروجين والأكسجين عن طريق الكهرباء. فالهيدروجين يوفِّر الطاقة للبكتريا لتحويل ثاني أكسيد الكربون والنيتروجين الموجودين في الهواء إلى مادة عضوية غنية بالبروتين بشكل أكفأ من نمو النباتات باستخدام البناء الضوئي. وهذا البروتين يشبه دقيق القمح وقد أطلق عليه اسم "سولين" (Solein).

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

وعلى هذا، فإن البروتينات المشتقة من الميكروبات سوف:

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

فتخيّل أن الميكروبات ستكون مصانع المستقبل، وأن غذاء المستقبل سيكون مصنوعاً من الهواء! وأن عام 2050م سيكون مختلفاً تماماً عن عالمنا اليوم. فهو عالم من دون زراعة ولا تربية حيوانات من أجل الغذاء! قد يبدو ذلك خيالياً لكنه ليس مستحيلاً!

حول

أقبلت، فأقبلت معك الحياة بجميع صنوفها وألوانها: فالنبات ينبت، والأشجار تورق وتزهر، والهرة تموء، والقمري يسجع، والغنم يثغو، والبقر يخور، وكل أليف يدعو أليفه. كل شيء يشعر بالحياة وينسي هموم الحياة، ولا يذكر إلا سعادة الحياة، فإن كان الزمان جسدا فأنت روحه، وإن كان عمرا فأنت شبابه.

في مكان آخر

  1. GitHub
  2. Social
  3. Facebook
================================================ FILE: site/src/assets/examples/breadcrumbs/breadcrumbs.css ================================================ .breadcrumb-chevron { --bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d'%3E%3Cpath fill-rule='evenodd' d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); gap: .5rem; } .breadcrumb-chevron .breadcrumb-item { display: flex; gap: inherit; align-items: center; padding-left: 0; line-height: 1; } .breadcrumb-chevron .breadcrumb-item::before { gap: inherit; float: none; width: 1rem; height: 1rem; } .breadcrumb-custom .breadcrumb-item { position: relative; flex-grow: 1; padding: .75rem 3rem; } .breadcrumb-custom .breadcrumb-item::before { display: none; } .breadcrumb-custom .breadcrumb-item::after { position: absolute; top: 50%; right: -25px; z-index: 1; display: inline-block; width: 50px; height: 50px; margin-top: -25px; content: ""; background-color: var(--bs-tertiary-bg); border-top-right-radius: .5rem; box-shadow: 1px -1px var(--bs-border-color); transform: scale(.707) rotate(45deg); } .breadcrumb-custom .breadcrumb-item:first-child { padding-left: 1.5rem; } .breadcrumb-custom .breadcrumb-item:last-child { padding-right: 1.5rem; } .breadcrumb-custom .breadcrumb-item:last-child::after { display: none; } ================================================ FILE: site/src/assets/examples/breadcrumbs/index.astro ================================================ --- export const title = 'Breadcrumbs' export const extra_css = ['breadcrumbs.css'] ---
================================================ FILE: site/src/assets/examples/buttons/index.astro ================================================ --- export const title = 'Buttons' ---
================================================ FILE: site/src/assets/examples/carousel/carousel.css ================================================ /* GLOBAL STYLES -------------------------------------------------- */ /* Padding below the footer and lighter body text */ body { padding-top: 3rem; padding-bottom: 3rem; color: rgb(var(--bs-tertiary-color-rgb)); } /* CUSTOMIZE THE CAROUSEL -------------------------------------------------- */ /* Carousel base class */ .carousel { margin-bottom: 4rem; } /* Since positioning the image, we need to help out the caption */ .carousel-caption { bottom: 3rem; z-index: 10; } /* Declare heights because of positioning of img element */ .carousel-item { height: 32rem; } /* MARKETING CONTENT -------------------------------------------------- */ /* Center align the text within the three columns below the carousel */ .marketing .col-lg-4 { margin-bottom: 1.5rem; text-align: center; } /* rtl:begin:ignore */ .marketing .col-lg-4 p { margin-right: .75rem; margin-left: .75rem; } /* rtl:end:ignore */ /* Featurettes ------------------------- */ .featurette-divider { margin: 5rem 0; /* Space out the Bootstrap
more */ } /* Thin out the marketing headings */ /* rtl:begin:remove */ .featurette-heading { letter-spacing: -.05rem; } /* rtl:end:remove */ /* RESPONSIVE CSS -------------------------------------------------- */ @media (min-width: 40em) { /* Bump up size of carousel content */ .carousel-caption p { margin-bottom: 1.25rem; font-size: 1.25rem; line-height: 1.4; } .featurette-heading { font-size: 50px; } } @media (min-width: 62em) { .featurette-heading { margin-top: 7rem; } } ================================================ FILE: site/src/assets/examples/carousel/carousel.rtl.css ================================================ /* GLOBAL STYLES -------------------------------------------------- */ /* Padding below the footer and lighter body text */ body { padding-top: 3rem; padding-bottom: 3rem; color: rgb(var(--bs-tertiary-color-rgb)); } /* CUSTOMIZE THE CAROUSEL -------------------------------------------------- */ /* Carousel base class */ .carousel { margin-bottom: 4rem; } /* Since positioning the image, we need to help out the caption */ .carousel-caption { bottom: 3rem; z-index: 10; } /* Declare heights because of positioning of img element */ .carousel-item { height: 32rem; } /* MARKETING CONTENT -------------------------------------------------- */ /* Center align the text within the three columns below the carousel */ .marketing .col-lg-4 { margin-bottom: 1.5rem; text-align: center; } .marketing .col-lg-4 p { margin-right: .75rem; margin-left: .75rem; } /* Featurettes ------------------------- */ .featurette-divider { margin: 5rem 0; /* Space out the Bootstrap
more */ } /* Thin out the marketing headings */ /* RESPONSIVE CSS -------------------------------------------------- */ @media (min-width: 40em) { /* Bump up size of carousel content */ .carousel-caption p { margin-bottom: 1.25rem; font-size: 1.25rem; line-height: 1.4; } .featurette-heading { font-size: 50px; } } @media (min-width: 62em) { .featurette-heading { margin-top: 7rem; } } ================================================ FILE: site/src/assets/examples/carousel/index.astro ================================================ --- export const title = 'Carousel Template' export const extra_css = ['carousel.css'] import Placeholder from "@shortcodes/Placeholder.astro" ---

Heading

Some representative placeholder content for the three columns of text below the carousel. This is the first column.

View details »

Heading

Another exciting bit of representative placeholder content. This time, we've moved on to the second column.

View details »

Heading

And lastly this, the third column of representative placeholder content.

View details »


First featurette heading. It’ll blow your mind.

Some great placeholder content for the first featurette here. Imagine some exciting prose here.


Oh yeah, it’s that good. See for yourself.

Another featurette? Of course. More placeholder content here to give you an idea of how this layout would work with some actual real-world content in place.


And lastly, this one. Checkmate.

And yes, this is the last block of representative placeholder content. Again, not really intended to be actually read, simply here to give you a better view of what this would look like with some actual content. Your content.


================================================ FILE: site/src/assets/examples/carousel-rtl/index.astro ================================================ --- export const title = 'قالب شرائح العرض' export const direction = 'rtl' export const extra_css = ['../carousel/carousel.rtl.css'] import Placeholder from "@shortcodes/Placeholder.astro" ---

عنوان

تذكر دائماً أن الحاسوب لا يمتلك ذكاءً، ولكنه يكتسب الذكاء الاصطناعي من خلال ثلاثة عناصر وظيفية رئيسة، هي: القدرة على التحليل، والقدرة على التأليف، والاستدلال المنطقي.

عرض التفاصيل

عنوان آخر

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

عرض التفاصيل

عنوان ثالث لتأكيد المعلومة

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

عرض التفاصيل


العنوان الأول المميز. سيذهل عقلك.

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


أوه نعم، هذا جيد. شاهد بنفسك.

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


وأخيرًا، هذا. كش ملك.

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


================================================ FILE: site/src/assets/examples/cheatsheet/cheatsheet.css ================================================ body { scroll-behavior: smooth; } /** * Bootstrap "Journal code" icon * @link https://icons.getbootstrap.com/icons/journal-code/ */ .bd-heading a::before { display: inline-block; width: 1em; height: 1em; margin-right: .25rem; content: ""; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M4 1h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1H2a2 2 0 0 1 2-2z'/%3E%3Cpath d='M2 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2z'/%3E%3Cpath fill-rule='evenodd' d='M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z'/%3E%3C/svg%3E"); background-size: 1em; } /* stylelint-disable-next-line selector-max-universal */ .bd-heading + div > * + * { margin-top: 3rem; } /* Table of contents */ .bd-aside a { padding: .1875rem .5rem; margin-top: .125rem; margin-left: .3125rem; color: var(--bs-body-color); } .bd-aside a:hover, .bd-aside a:focus { color: var(--bs-body-color); background-color: rgba(121, 82, 179, .1); } .bd-aside .active { font-weight: 600; color: var(--bs-body-color); } .bd-aside .btn { padding: .25rem .5rem; font-weight: 600; color: var(--bs-body-color); } .bd-aside .btn:hover, .bd-aside .btn:focus { color: var(--bs-body-color); background-color: rgba(121, 82, 179, .1); } .bd-aside .btn:focus { box-shadow: 0 0 0 1px rgba(121, 82, 179, .7); } .bd-aside .btn::before { width: 1.25em; line-height: 0; content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ccc' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); transition: transform .35s ease; /* rtl:raw: transform: rotate(180deg) translateX(-2px); */ transform-origin: .5em 50%; } .bd-aside .btn[aria-expanded="true"]::before { transform: rotate(90deg)/* rtl:ignore */; } /* Examples */ .scrollspy-example { height: 200px; } [id="modal"] .bd-example .btn, [id="buttons"] .bd-example .btn, [id="tooltips"] .bd-example .btn, [id="popovers"] .bd-example .btn, [id="dropdowns"] .bd-example .btn-group, [id="dropdowns"] .bd-example .dropdown, [id="dropdowns"] .bd-example .dropup, [id="dropdowns"] .bd-example .dropend, [id="dropdowns"] .bd-example .dropstart { margin: 0 1rem 1rem 0; } /* Layout */ @media (min-width: 1200px) { body { display: grid; grid-template-rows: auto; grid-template-columns: 1fr 4fr 1fr; gap: 1rem; } .bd-header { position: fixed; top: 0; /* rtl:begin:ignore */ right: 0; left: 0; /* rtl:end:ignore */ z-index: 1030; grid-column: 1 / span 3; } .bd-aside, .bd-cheatsheet { padding-top: 4rem; } /** * 1. Too bad only Firefox supports subgrids ATM */ .bd-cheatsheet, .bd-cheatsheet section, .bd-cheatsheet article { display: inherit; /* 1 */ grid-template-rows: auto; grid-template-columns: 1fr 4fr; grid-column: 1 / span 2; gap: inherit; /* 1 */ } .bd-aside { grid-area: 1 / 3; scroll-margin-top: 4rem; } .bd-cheatsheet section, .bd-cheatsheet section > h2 { top: 2rem; scroll-margin-top: 2rem; } .bd-cheatsheet section > h2::before { position: absolute; /* rtl:begin:ignore */ top: 0; right: 0; bottom: -2rem; left: 0; /* rtl:end:ignore */ z-index: -1; content: ""; } .bd-cheatsheet article, .bd-cheatsheet .bd-heading { top: 8rem; scroll-margin-top: 8rem; } .bd-cheatsheet .bd-heading { z-index: 1; } } ================================================ FILE: site/src/assets/examples/cheatsheet/cheatsheet.js ================================================ /* global bootstrap: false */ (() => { 'use strict' // Tooltip and popover demos document.querySelectorAll('.tooltip-demo') .forEach(tooltip => { new bootstrap.Tooltip(tooltip, { selector: '[data-bs-toggle="tooltip"]' }) }) document.querySelectorAll('[data-bs-toggle="popover"]') .forEach(popover => { new bootstrap.Popover(popover) }) document.querySelectorAll('.toast') .forEach(toastNode => { const toast = new bootstrap.Toast(toastNode, { autohide: false }) toast.show() }) // Disable empty links and submit buttons document.querySelectorAll('[href="#"], [type="submit"]') .forEach(link => { link.addEventListener('click', event => { event.preventDefault() }) }) function setActiveItem() { const { hash } = window.location if (hash === '') { return } const link = document.querySelector(`.bd-aside a[href="${hash}"]`) if (!link) { return } const active = document.querySelector('.bd-aside .active') const parent = link.parentNode.parentNode.previousElementSibling link.classList.add('active') if (parent.classList.contains('collapsed')) { parent.click() } if (!active) { return } const expanded = active.parentNode.parentNode.previousElementSibling active.classList.remove('active') if (expanded && parent !== expanded) { expanded.click() } } setActiveItem() window.addEventListener('hashchange', setActiveItem) })() ================================================ FILE: site/src/assets/examples/cheatsheet/cheatsheet.rtl.css ================================================ body { scroll-behavior: smooth; } /** * Bootstrap "Journal code" icon * @link https://icons.getbootstrap.com/icons/journal-code/ */ .bd-heading a::before { display: inline-block; width: 1em; height: 1em; margin-left: .25rem; content: ""; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M4 1h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1H2a2 2 0 0 1 2-2z'/%3E%3Cpath d='M2 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2z'/%3E%3Cpath fill-rule='evenodd' d='M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z'/%3E%3C/svg%3E"); background-size: 1em; } /* stylelint-disable-next-line selector-max-universal */ .bd-heading + div > * + * { margin-top: 3rem; } /* Table of contents */ .bd-aside a { padding: .1875rem .5rem; margin-top: .125rem; margin-right: .3125rem; color: var(--bs-body-color); } .bd-aside a:hover, .bd-aside a:focus { color: var(--bs-body-color); background-color: rgba(121, 82, 179, .1); } .bd-aside .active { font-weight: 600; color: var(--bs-body-color); } .bd-aside .btn { padding: .25rem .5rem; font-weight: 600; color: var(--bs-body-color); } .bd-aside .btn:hover, .bd-aside .btn:focus { color: var(--bs-body-color); background-color: rgba(121, 82, 179, .1); } .bd-aside .btn:focus { box-shadow: 0 0 0 1px rgba(121, 82, 179, .7); } .bd-aside .btn::before { width: 1.25em; line-height: 0; content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ccc' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); transition: transform .35s ease; transform: rotate(180deg) translateX(-2px); transform-origin: .5em 50%; } .bd-aside .btn[aria-expanded="true"]::before { transform: rotate(90deg); } /* Examples */ .scrollspy-example { height: 200px; } [id="modal"] .bd-example .btn, [id="buttons"] .bd-example .btn, [id="tooltips"] .bd-example .btn, [id="popovers"] .bd-example .btn, [id="dropdowns"] .bd-example .btn-group, [id="dropdowns"] .bd-example .dropdown, [id="dropdowns"] .bd-example .dropup, [id="dropdowns"] .bd-example .dropend, [id="dropdowns"] .bd-example .dropstart { margin: 0 0 1rem 1rem; } /* Layout */ @media (min-width: 1200px) { body { display: grid; grid-template-rows: auto; grid-template-columns: 1fr 4fr 1fr; gap: 1rem; } .bd-header { position: fixed; top: 0; right: 0; left: 0; z-index: 1030; grid-column: 1 / span 3; } .bd-aside, .bd-cheatsheet { padding-top: 4rem; } /** * 1. Too bad only Firefox supports subgrids ATM */ .bd-cheatsheet, .bd-cheatsheet section, .bd-cheatsheet article { display: inherit; /* 1 */ grid-template-rows: auto; grid-template-columns: 1fr 4fr; grid-column: 1 / span 2; gap: inherit; /* 1 */ } .bd-aside { grid-area: 1 / 3; scroll-margin-top: 4rem; } .bd-cheatsheet section, .bd-cheatsheet section > h2 { top: 2rem; scroll-margin-top: 2rem; } .bd-cheatsheet section > h2::before { position: absolute; top: 0; right: 0; bottom: -2rem; left: 0; z-index: -1; content: ""; } .bd-cheatsheet article, .bd-cheatsheet .bd-heading { top: 8rem; scroll-margin-top: 8rem; } .bd-cheatsheet .bd-heading { z-index: 1; } } ================================================ FILE: site/src/assets/examples/cheatsheet/index.astro ================================================ --- import { getData } from '@libs/data' import { getVersionedDocsPath } from '@libs/path' import Example from '@shortcodes/Example.astro' import Placeholder from '@shortcodes/Placeholder.astro' export const title = 'Cheatsheet' export const extra_css = ['cheatsheet.css'] export const extra_js = [{ src: 'cheatsheet.js' }] export const body_class = 'bg-body-tertiary' ---

Bootstrap Cheatsheet

RTL cheatsheet

Contents

Typography

Documentation
Display 1

Display 2

Display 3

Display 4

Display 5

Display 6

`} /> Heading 1

Heading 2

Heading 3

Heading 4

Heading 5

Heading 6

`} /> This is a lead paragraph. It stands out from regular paragraphs.

`} /> 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.

`} /> `} />

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

Someone famous in Source Title
`} />
  • 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.
  • `} />
  • This is a list item.
  • And another one.
  • But they're displayed inline.
  • `} />

    Tables

    Documentation
    # First Last Handle 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} /> # First Last Handle 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} /> Class Heading Heading Default Cell Cell `, ...getData('theme-colors').map((themeColor) => ` ${themeColor.title} Cell Cell `), ` `]} /> # First Last Handle 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} />

    Forms

    Overview

    Documentation
    We'll never share your email with anyone else.
    Radios buttons
    `} />

    Disabled forms

    Documentation
    Disabled radios buttons
    `} />

    Sizing

    Documentation
    `} />
    `} />

    Input group

    Documentation
    @
    @example.com
    https://example.com/users/
    $ .00
    With textarea
    `} />

    Validation

    Documentation
    Looks good!
    Looks good!
    @
    Please choose a username.
    Please provide a valid city.
    Please select a valid state.
    Please provide a valid zip.
    You must agree before submitting.
    `} />

    Components

    Accordion

    Documentation

    This is the first item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the second item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the third item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.
    `} />

    Alerts

    Documentation
    ` `)} />

    Well done!

    Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.


    Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

    `} />
    Example heading New

    Example heading New

    Example heading New

    Example heading New

    Example heading New

    Example heading New

    Example heading New

    Example heading New

    `} /> ` ${themeColor.title} `)} />

    Buttons

    Documentation
    ` `), ``]} /> ` `)} /> Small button `} />
    Card title

    Some quick example text to build on the card title and make up the bulk of the card's content.

    Go somewhere
    Featured
    Card title

    Some quick example text to build on the card title and make up the bulk of the card's content.

    Go somewhere
    Card title

    Some quick example text to build on the card title and make up the bulk of the card's content.

    • An item
    • A second item
    • A third item
    Card title

    This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

    Last updated 3 mins ago

    `} />

    Popovers

    Documentation
    Click to toggle popover `} /> Popover on top `} />

    Scrollspy

    Documentation
    Navbar

    First heading

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    Second heading

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    Third heading

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    Fourth heading

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    Fifth heading

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    `} />

    Toasts

    Documentation
    Bootstrap 11 mins ago
    Hello, world! This is a toast message.
    `} />

    Tooltips

    Documentation
    Tooltip on top `} />
    ================================================ FILE: site/src/assets/examples/cheatsheet-rtl/index.astro ================================================ --- import { getData } from '@libs/data' import { getVersionedDocsPath } from '@libs/path' import Example from '@shortcodes/Example.astro' export const title = 'ورقة الغش' export const extra_css = ['../cheatsheet/cheatsheet.rtl.css'] export const extra_js = [{src: '../cheatsheet/cheatsheet.js'}] export const body_class = 'bg-body-tertiary' export const direction = 'rtl' import Placeholder from "@shortcodes/Placeholder.astro" ---

    Bootstrap ورقة الغش

    جدول بيانات LTR

    المحتوى

    العرض 1

    العرض 2

    العرض 3

    العرض 4

    العرض 5

    العرض 6

    `} /> عنوان 1

    عنوان 2

    عنوان 3

    عنوان 4

    عنوان 5

    عنوان 6

    `} /> هذه قطعة إملائية متميزة، فهي مصممة لتكون بارزة من بين القطع الإملائية الأخرى.

    `} /> يمكنك استخدام تصنيف mark لتحديد نص.

    من المفترض أن يتم التعامل مع هذا السطر كنص محذوف.

    من المفترض أن يتم التعامل مع هذا السطر على أنه لم يعد دقيقًا.

    من المفترض أن يتم التعامل مع هذا السطر كإضافة إلى المستند.

    سيتم عرض النص في هذا السطر كما وتحته خط.

    من المفترض أن يتم التعامل مع هذا السطر على أنه يحوي تفاصيل صغيرة.

    هذا السطر يحوي نص عريض.

    هذا السطر يحوي نص مائل.

    `} /> `} />

    إقتباس مبهر، موضوع في عنصر blockquote

    شخص مشهور في عنوان المصدر
    `} />
  • هذه قائمة عناصر.
  • بالرغم من أنها مصممة كي لا تظهر كذلك.
  • إلا أنها مجهزة كـ قائمة خلف الكواليس
  • هذا التصميم ينطبق فقد على القائمة الرئيسية
  • القوائم الفرعية
    • لا تتأثر بهذا التصميم
    • فهي تظهر عليها علامات الترقيم
    • وتحتوي على مساحة فارغة بجوارها
  • قد يكون هذا التصميم مفيدًا في بعض الأحيان.
  • `} />
  • هذا عنصر في قائمة.
  • وهذا أيضًا.
  • لكنهم يظهرون متجاورين.
  • `} />
    # الاسم الاول الكنية الاسم المستعار 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} /> # الاسم الاول الكنية الاسم المستعار 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} /> Class عنوان عنوان Default خلية خلية `, ...getData('theme-colors').map((themeColor) => ` ${themeColor.title} خلية خلية `), ` `]} /> # الاسم الاول الكنية الاسم المستعار 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social `} />

    النماذج

    نظرة عامة

    دليل الإستخدام
    لن نقوم بمشاركة بريدك الإلكتروني مع أي شخص آخر.
    أزرار الاختيار الأحادي
    `} />

    الحقول المعطلة

    دليل الإستخدام
    أزرار اختيار أحادي معطلين
    `} />
    `} />
    `} />

    مجموعة الإدخال

    دليل الإستخدام
    أنا اسمي
    وغيرها
    https://example.com/users/
    .00 $
    مع textarea
    `} />

    الحقول ذوي العناوين العائمة

    دليل الإستخدام
    `} />
    يبدو صحيحًا!
    يبدو صحيحًا!
    @
    يرجى اختيار اسم مستخدم.
    يرجى إدخال مدينة صحيحة.
    يرجى اختيار ولاية صحيحة.
    يرجى إدخال رمز بريدي صحيح.
    تجب الموافقة قبل إرسال النموذج.
    `} />

    العناصر

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

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

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

    الإنذارات

    دليل الإستخدام
    ` `)} />

    أحسنت!

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


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

    `} />
    مثال على عنوان جديد

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

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

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

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

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

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

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

    `} /> ` ${themeColor.title} `)} />
    ` `), ``]} /> ` `)} /> زر صغير `} />
    عنوان البطاقة

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

    اذهب لمكان ما
    متميز
    عنوان البطاقة

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

    اذهب لمكان ما
    عنوان البطاقة

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

    • عنصر
    • عنصر آخر
    • عنصر ثالث
    عنوان البطاقة

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

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

    `} />

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

    دليل الإستخدام
    انقر لعرض/إخفاء الصندوق المنبثق `} /> انبثاق إلى الأعلى `} />

    المخطوطة

    دليل الإستخدام
    شريط التنقل

    @fat

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

    @mdo

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

    واحد

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

    اثنان

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

    ثلاثة

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

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

    `} />

    الإشعارات

    دليل الإستخدام
    Bootstrap قبل 11 دقيقة
    مرحبًا بالعالم! هذه رسالة إشعار.
    `} />

    التلميحات

    دليل الإستخدام
    تلميح يظهر في الأعلى `} />
    ================================================ FILE: site/src/assets/examples/checkout/checkout.css ================================================ .container { max-width: 960px; } ================================================ FILE: site/src/assets/examples/checkout/checkout.js ================================================ // Example starter JavaScript for disabling form submissions if there are invalid fields (() => { 'use strict' // Fetch all the forms we want to apply custom Bootstrap validation styles to const forms = document.querySelectorAll('.needs-validation') // Loop over them and prevent submission Array.from(forms).forEach(form => { form.addEventListener('submit', event => { if (!form.checkValidity()) { event.preventDefault() event.stopPropagation() } form.classList.add('was-validated') }, false) }) })() ================================================ FILE: site/src/assets/examples/checkout/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Checkout example' export const extra_css = ['checkout.css'] export const extra_js = [{ src: 'checkout.js' }] export const body_class = 'bg-body-tertiary' ---

    Checkout form

    Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.

    Your cart 3

    • Product name
      Brief description
      $12
    • Second product
      Brief description
      $8
    • Third item
      Brief description
      $5
    • Promo code
      EXAMPLECODE
      −$5
    • Total (USD) $20

    Billing address

    Valid first name is required.
    Valid last name is required.
    @
    Your username is required.
    Please enter a valid email address for shipping updates.
    Please enter your shipping address.
    Please select a valid country.
    Please provide a valid state.
    Zip code required.


    Payment

    Full name as displayed on card
    Name on card is required
    Credit card number is required
    Expiration date required
    Security code required

    ================================================ FILE: site/src/assets/examples/checkout-rtl/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'مثال إتمام الشراء' export const direction = 'rtl' export const extra_css = ['../checkout/checkout.css'] export const extra_js = [{ src: '../checkout/checkout.js' }] export const body_class = 'bg-body-tertiary' ---

    نموذج إتمام الشراء

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

    عربة التسوق 3

    • اسم المنتج
      وصف مختصر
      $12
    • المنتج الثاني
      وصف مختصر
      $8
    • البند الثالث
      وصف مختصر
      $5
    • رمز ترويجي
      EXAMPLECODE
      -$5
    • مجموع (USD) $20

    عنوان الفوترة

    يرجى إدخال اسم أول صحيح.
    يرجى إدخال اسم عائلة صحيح.
    @
    اسم المستخدم الخاص بك مطلوب.
    يرجى إدخال عنوان بريد إلكتروني صحيح لتصلكم تحديثات الشحن.
    يرجى إدخال عنوان الشحن الخاص بك.
    يرجى اختيار بلد صحيح.
    يرجى اختيار اسم منطقة صحيح.
    الرمز البريدي مطلوب.


    طريقة الدفع

    الاسم الكامل كما هو معروض على البطاقة
    الاسم على البطاقة مطلوب
    رقم بطاقة الائتمان مطلوب
    تاريخ انتهاء الصلاحية مطلوب
    رمز الحماية مطلوب

    ================================================ FILE: site/src/assets/examples/cover/cover.css ================================================ /* * Globals */ /* Custom default button */ .btn-light, .btn-light:hover, .btn-light:focus { color: #333; text-shadow: none; /* Prevent inheritance from `body` */ } /* * Base structure */ body { text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); } .cover-container { max-width: 42em; } /* * Header */ .nav-masthead .nav-link { color: rgba(255, 255, 255, .5); border-bottom: .25rem solid transparent; } .nav-masthead .nav-link:hover, .nav-masthead .nav-link:focus { border-bottom-color: rgba(255, 255, 255, .25); } .nav-masthead .nav-link + .nav-link { margin-left: 1rem; } .nav-masthead .active { color: #fff; border-bottom-color: #fff; } ================================================ FILE: site/src/assets/examples/cover/index.astro ================================================ --- export const title = 'Cover Template' export const extra_css = ['cover.css'] export const html_class = 'h-100' export const body_class = 'd-flex h-100 text-center text-bg-dark' ---

    Cover your page.

    Cover is a one-page template for building simple and beautiful home pages. Download, edit the text, and add your own fullscreen background photo to make it your own.

    Learn more

    ================================================ FILE: site/src/assets/examples/dashboard/dashboard.css ================================================ .bi { display: inline-block; width: 1rem; height: 1rem; } /* * Sidebar */ @media (min-width: 768px) { .sidebar .offcanvas-lg { position: -webkit-sticky; position: sticky; top: 48px; } .navbar-search { display: block; } } .sidebar .nav-link { font-size: .875rem; font-weight: 500; } .sidebar .nav-link.active { color: #2470dc; } .sidebar-heading { font-size: .75rem; } /* * Navbar */ .navbar-brand { padding-top: .75rem; padding-bottom: .75rem; background-color: rgba(0, 0, 0, .25); box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); } .navbar .form-control { padding: .75rem 1rem; } ================================================ FILE: site/src/assets/examples/dashboard/dashboard.js ================================================ /* globals Chart:false */ (() => { 'use strict' // Graphs const ctx = document.getElementById('myChart') new Chart(ctx, { type: 'line', data: { labels: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ], datasets: [{ data: [ 15339, 21345, 18483, 24003, 23489, 24092, 12034 ], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { plugins: { legend: { display: false }, tooltip: { boxPadding: 3 } } } }) })() ================================================ FILE: site/src/assets/examples/dashboard/dashboard.rtl.css ================================================ .bi { display: inline-block; width: 1rem; height: 1rem; } /* * Sidebar */ @media (min-width: 768px) { .sidebar .offcanvas-lg { position: -webkit-sticky; position: sticky; top: 48px; } .navbar-search { display: block; } } .sidebar .nav-link { font-size: .875rem; font-weight: 500; } .sidebar .nav-link.active { color: #2470dc; } .sidebar-heading { font-size: .75rem; } /* * Navbar */ .navbar-brand { padding-top: .75rem; padding-bottom: .75rem; background-color: rgba(0, 0, 0, .25); box-shadow: inset 1px 0 0 rgba(0, 0, 0, .25); } .navbar .form-control { padding: .75rem 1rem; } ================================================ FILE: site/src/assets/examples/dashboard/index.astro ================================================ --- export const title = 'Dashboard Template' export const extra_css = ['dashboard.css'] export const extra_js = [ { src: 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js', integrity: 'sha384-eI7PSr3L1XLISH8JdDII5YN/njoSsxfbrkCTnJrzXt+ENP5MOVBxD+l6sEG4zoLp'}, { src: 'dashboard.js'} ] ---

    Dashboard

    Section title

    # Header Header Header Header
    1,001 random data placeholder text
    1,002 placeholder irrelevant visual layout
    1,003 data rich dashboard tabular
    1,003 information placeholder illustrative data
    1,004 text random layout dashboard
    1,005 dashboard irrelevant text placeholder
    1,006 dashboard illustrative rich data
    1,007 placeholder tabular information irrelevant
    1,008 random data placeholder text
    1,009 placeholder irrelevant visual layout
    1,010 data rich dashboard tabular
    1,011 information placeholder illustrative data
    1,012 text placeholder layout dashboard
    1,013 dashboard irrelevant text visual
    1,014 dashboard illustrative rich data
    1,015 random tabular information text
    ================================================ FILE: site/src/assets/examples/dashboard-rtl/dashboard.js ================================================ /* globals Chart:false */ (() => { 'use strict' // Graphs const ctx = document.getElementById('myChart') new Chart(ctx, { type: 'line', data: { labels: [ 'الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت' ], datasets: [{ data: [ 15339, 21345, 18483, 24003, 23489, 24092, 12034 ], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { plugins: { legend: { display: false }, tooltip: { boxPadding: 3 } } } }) })() ================================================ FILE: site/src/assets/examples/dashboard-rtl/index.astro ================================================ --- export const title = 'قالب لوحة القيادة' export const direction = 'rtl' export const extra_css = ['../dashboard/dashboard.rtl.css'] export const extra_js = [ { src: 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js', integrity: 'sha384-eI7PSr3L1XLISH8JdDII5YN/njoSsxfbrkCTnJrzXt+ENP5MOVBxD+l6sEG4zoLp'}, { 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 بيانات مفيدة معلومات الجدول
    ================================================ FILE: site/src/assets/examples/dropdowns/dropdowns.css ================================================ .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 { color: var(--bs-body-color); background-color: var(--bs-body-bg); } .btn-hover-light:hover, .btn-hover-light:focus { color: var(--bs-link-hover-color); background-color: var(--bs-tertiary-bg); } .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; color: var(--bs-emphasis-color); } .cal-btn:hover, .cal-btn:focus { background-color: var(--bs-secondary-bg); } .cal-btn[disabled] { border: 0; opacity: .5; } .w-220px { width: 220px; } .w-280px { width: 280px; } .w-340px { width: 340px; } .opacity-10 { opacity: .1; } ================================================ FILE: site/src/assets/examples/dropdowns/index.astro ================================================ --- export const title = 'Dropdowns' export const extra_css = ['dropdowns.css'] ---
    ================================================ FILE: site/src/assets/examples/features/features.css ================================================ .feature-icon { width: 4rem; height: 4rem; border-radius: .75rem; } .icon-square { width: 3rem; height: 3rem; border-radius: .75rem; } .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; } .feature-icon-small { width: 3rem; height: 3rem; } ================================================ FILE: site/src/assets/examples/features/index.astro ================================================ --- export const title = 'Features' export const extra_css = ['features.css'] --- Bootstrap

    Features examples

    Hanging icons

    Featured title

    Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

    Primary button

    Featured title

    Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

    Primary button

    Featured title

    Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

    Primary button

    Custom cards

    Short title, long jacket

    • Bootstrap
    • Earth
    • 3d

    Much longer title that wraps to multiple lines

    • Bootstrap
    • Pakistan
    • 4d

    Another longer title belongs here

    • Bootstrap
    • California
    • 5d

    Icon grid

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Features with title

    Left-aligned title explaining these awesome features

    Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

    Primary button

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    Featured title

    Paragraph of text beneath the heading to explain the heading.

    ================================================ FILE: site/src/assets/examples/footers/index.astro ================================================ --- export const title = 'Footers' --- Bootstrap
    © {new Date().getFullYear()} Company, Inc
    ================================================ FILE: site/src/assets/examples/grid/grid.css ================================================ .themed-grid-col { padding-top: .75rem; padding-bottom: .75rem; background-color: rgba(112.520718, 44.062154, 249.437846, .15); border: 1px solid rgba(112.520718, 44.062154, 249.437846, .3); } .themed-container { padding: .75rem; margin-bottom: 1.5rem; background-color: rgba(112.520718, 44.062154, 249.437846, .15); border: 1px solid rgba(112.520718, 44.062154, 249.437846, .3); } ================================================ FILE: site/src/assets/examples/grid/index.astro ================================================ --- export const title = 'Grid Template' export const extra_css = ['grid.css'] export const body_class = 'py-4' ---

    Bootstrap grid examples

    Basic grid layouts to get you familiar with building within the Bootstrap grid system.

    In these examples the .themed-grid-col class is added to the columns to add some theming. This is not a class that is available in Bootstrap by default.

    Five grid tiers

    There are five tiers to the Bootstrap grid system, one for each range of devices we support. Each tier starts at a minimum viewport size and automatically applies to the larger devices unless overridden.

    .col-4
    .col-4
    .col-4
    .col-sm-4
    .col-sm-4
    .col-sm-4
    .col-md-4
    .col-md-4
    .col-md-4
    .col-lg-4
    .col-lg-4
    .col-lg-4
    .col-xl-4
    .col-xl-4
    .col-xl-4
    .col-xxl-4
    .col-xxl-4
    .col-xxl-4

    Three equal columns

    Get three equal-width columns starting at desktops and scaling to large desktops. On mobile devices, tablets and below, the columns will automatically stack.

    .col-md-4
    .col-md-4
    .col-md-4

    Three equal columns alternative

    By using the .row-cols-* classes, you can easily create a grid with equal columns.

    .col child of .row-cols-md-3
    .col child of .row-cols-md-3
    .col child of .row-cols-md-3

    Three unequal columns

    Get three columns starting at desktops and scaling to large desktops of various widths. Remember, grid columns should add up to twelve for a single horizontal block. More than that, and columns start stacking no matter the viewport.

    .col-md-3
    .col-md-6
    .col-md-3

    Two columns

    Get two columns starting at desktops and scaling to large desktops.

    .col-md-8
    .col-md-4

    Full width, single column

    No grid classes are necessary for full-width elements.


    Two columns with two nested columns

    Per the documentation, nesting is easy—just put a row of columns within an existing column. This gives you two columns starting at desktops and scaling to large desktops, with another two (equal widths) within the larger column.

    At mobile device sizes, tablets and down, these columns and their nested columns will stack.

    .col-md-8
    .col-md-6
    .col-md-6
    .col-md-4

    Mixed: mobile and desktop

    The Bootstrap v5 grid system has six tiers of classes: xs (extra small, this class infix is not used), sm (small), md (medium), lg (large), xl (x-large), and xxl (xx-large). You can use nearly any combination of these classes to create more dynamic and flexible layouts.

    Each tier of classes scales up, meaning if you plan on setting the same widths for md, lg, xl and xxl, you only need to specify md.

    .col-md-8
    .col-6 .col-md-4
    .col-6 .col-md-4
    .col-6 .col-md-4
    .col-6 .col-md-4
    .col-6
    .col-6

    Mixed: mobile, tablet, and desktop

    .col-sm-6 .col-lg-8
    .col-6 .col-lg-4
    .col-6 .col-sm-4
    .col-6 .col-sm-4
    .col-6 .col-sm-4

    Gutters

    With .gx-* classes, the horizontal gutters can be adjusted.

    .col with .gx-4 gutters
    .col with .gx-4 gutters
    .col with .gx-4 gutters
    .col with .gx-4 gutters
    .col with .gx-4 gutters
    .col with .gx-4 gutters

    Use the .gy-* classes to control the vertical gutters.

    .col with .gy-4 gutters
    .col with .gy-4 gutters
    .col with .gy-4 gutters
    .col with .gy-4 gutters
    .col with .gy-4 gutters
    .col with .gy-4 gutters

    With .g-* classes, the gutters in both directions can be adjusted.

    .col with .g-3 gutters
    .col with .g-3 gutters
    .col with .g-3 gutters
    .col with .g-3 gutters
    .col with .g-3 gutters
    .col with .g-3 gutters

    Containers

    Additional classes added in Bootstrap v4.4 allow containers that are 100% wide until a particular breakpoint. v5 adds a new xxl breakpoint.

    .container
    .container-sm
    .container-md
    .container-lg
    .container-xl
    .container-xxl
    .container-fluid
    ================================================ FILE: site/src/assets/examples/headers/headers.css ================================================ .form-control-dark { border-color: var(--bs-gray); } .form-control-dark:focus { border-color: #fff; box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25); } .text-small { font-size: 85%; } .dropdown-toggle:not(:focus) { outline: 0; } ================================================ FILE: site/src/assets/examples/headers/index.astro ================================================ --- export const title = 'Headers' export const extra_css = ['headers.css'] --- Bootstrap

    Headers examples





















    ================================================ FILE: site/src/assets/examples/heroes/heroes.css ================================================ @media (min-width: 992px) { .rounded-lg-3 { border-radius: .3rem; } } ================================================ FILE: site/src/assets/examples/heroes/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Heroes' export const extra_css = ['heroes.css'] ---

    Heroes examples

    Centered hero

    Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

    Centered screenshot

    Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

    Example image
    Bootstrap Themes

    Responsive left-aligned hero with image

    Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

    Vertically centered hero sign-up form

    Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.


    By clicking Sign up, you agree to the terms of use.

    Border hero with cropped image and shadows

    Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

    Dark color hero

    Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

    ================================================ FILE: site/src/assets/examples/jumbotron/index.astro ================================================ --- export const title = 'Jumbotron example' ---
    Bootstrap Jumbotron example

    Custom jumbotron

    Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.

    Change the background

    Swap the background-color utility and add a `.text-*` color utility to mix up the jumbotron look. Then, mix and match with additional component themes and more.

    Add borders

    Or, keep it light and add a border for some added definition to the boundaries of your content. Be sure to look under the hood at the source HTML here as we've adjusted the alignment and sizing of both column's content for equal-height.

    © {new Date().getFullYear()}
    ================================================ FILE: site/src/assets/examples/jumbotrons/index.astro ================================================ --- export const title = 'Jumbotrons' export const extra_css = ['jumbotrons.css'] --- Bootstrap

    Jumbotron with icon

    This is a custom jumbotron featuring an SVG image at the top, some longer text that wraps early thanks to a responsive .col-* class, and a customized call to action.

    Placeholder jumbotron

    This faded back jumbotron is useful for placeholder content. It's also a great way to add a bit of context to a page or section when no content is available and to encourage visitors to take a specific action.

    Full-width jumbotron

    This takes the basic jumbotron above and makes its background edge-to-edge with a .container inside to align content. Similar to above, it's been recreated with built-in grid and utility classes.

    Basic jumbotron

    This is a simple Bootstrap jumbotron that sits within a .container, recreated with built-in utility classes.

    ================================================ FILE: site/src/assets/examples/jumbotrons/jumbotrons.css ================================================ .border-dashed { --bs-border-style: dashed; } ================================================ FILE: site/src/assets/examples/list-groups/index.astro ================================================ --- export const title = 'List groups' export const extra_css = ['list-groups.css'] ---
    ================================================ FILE: site/src/assets/examples/list-groups/list-groups.css ================================================ .list-group { width: 100%; max-width: 460px; margin-inline: 1.5rem; } .form-check-input:checked + .form-checked-content { opacity: .5; } .form-check-input-placeholder { border-style: dashed; } [contenteditable]:focus { outline: 0; } .list-group-checkable .list-group-item { cursor: pointer; } .list-group-item-check { position: absolute; clip: rect(0, 0, 0, 0); } .list-group-item-check:hover + .list-group-item { background-color: var(--bs-secondary-bg); } .list-group-item-check:checked + .list-group-item { color: #fff; background-color: var(--bs-primary); border-color: var(--bs-primary); } .list-group-item-check[disabled] + .list-group-item, .list-group-item-check:disabled + .list-group-item { pointer-events: none; filter: none; opacity: .5; } .list-group-radio .list-group-item { cursor: pointer; border-radius: .5rem; } .list-group-radio .form-check-input { z-index: 2; margin-top: -.5em; } .list-group-radio .list-group-item:hover, .list-group-radio .list-group-item:focus { background-color: var(--bs-secondary-bg); } .list-group-radio .form-check-input:checked + .list-group-item { background-color: var(--bs-body); border-color: var(--bs-primary); box-shadow: 0 0 0 2px var(--bs-primary); } .list-group-radio .form-check-input[disabled] + .list-group-item, .list-group-radio .form-check-input:disabled + .list-group-item { pointer-events: none; filter: none; opacity: .5; } ================================================ FILE: site/src/assets/examples/masonry/index.astro ================================================ --- export const title = 'Masonry example' export const extra_js = [{ src: 'https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js', integrity: 'sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D', async: true }] import Placeholder from "@shortcodes/Placeholder.astro" ---

    Bootstrap and Masonry

    Integrate Masonry with the Bootstrap grid system and cards component.

    Masonry is not included in Bootstrap. Add it by including the JavaScript plugin manually, or using a CDN like so:

    
    <script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
      

    By adding data-masonry='{"percentPosition": true }' to the .row wrapper, we can combine the powers of Bootstrap's responsive grid and Masonry's positioning.


    Card title that wraps to a new line

    This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

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

    Card title

    This card has supporting text below as a natural lead-in to additional content.

    Last updated 3 mins ago

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

    Card title

    This card has a regular title and short paragraph of text below it.

    Last updated 3 mins ago

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

    Card title

    This is another card with title and supporting text below. This card has some additional content to make it slightly taller overall.

    Last updated 3 mins ago

    ================================================ FILE: site/src/assets/examples/modals/index.astro ================================================ --- export const title = 'Modals' export const extra_css = ['modals.css'] ---
    ================================================ FILE: site/src/assets/examples/modals/modals.css ================================================ .modal-sheet .modal-dialog { width: 380px; transition: bottom .75s ease-in-out; } .modal-sheet .modal-footer { padding-bottom: 2rem; } ================================================ FILE: site/src/assets/examples/navbar-bottom/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Bottom navbar example' ---

    Bottom Navbar example

    This example is a quick exercise to illustrate how the bottom navbar works.

    View navbar docs »
    ================================================ FILE: site/src/assets/examples/navbar-fixed/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Fixed top navbar example' export const extra_css = ['navbar-fixed.css'] ---

    Navbar example

    This example is a quick exercise to illustrate how fixed to top navbar works. As you scroll, it will remain fixed to the top of your browser’s viewport.

    View navbar docs »
    ================================================ FILE: site/src/assets/examples/navbar-fixed/navbar-fixed.css ================================================ /* Show it is fixed to the top */ body { min-height: 75rem; padding-top: 4.5rem; } ================================================ FILE: site/src/assets/examples/navbar-static/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Top navbar example' export const extra_css = ['navbar-static.css'] ---

    Navbar example

    This example is a quick exercise to illustrate how the top-aligned navbar works. As you scroll, this navbar remains in its original position and moves with the rest of the page.

    View navbar docs »
    ================================================ FILE: site/src/assets/examples/navbar-static/navbar-static.css ================================================ /* Show it's not fixed to the top */ body { min-height: 75rem; } ================================================ FILE: site/src/assets/examples/navbars/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Navbar Template' export const extra_css = ['navbars.css'] ---

    Matching .container-xl...

    Navbar examples

    This example is a quick exercise to illustrate how the navbar and its contents work. Some navbars extend the width of the viewport, others are confined within a .container. For positioning of navbars, checkout the top and fixed top examples.

    At the smallest breakpoint, the collapse plugin is used to hide the links and show a menu button to toggle the collapsed content.

    View navbar docs »

    ================================================ FILE: site/src/assets/examples/navbars/navbars.css ================================================ body { padding-bottom: 20px; } .navbar { margin-bottom: 20px; } ================================================ FILE: site/src/assets/examples/navbars-offcanvas/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Navbar Template' export const extra_css = ['navbars-offcanvas.css'] ---

    Navbar with offcanvas examples

    This example shows how responsive offcanvas menus work within the navbar. For positioning of navbars, checkout the top and fixed top examples.

    From the top down, you'll see a dark navbar, light navbar and a responsive navbar—each with offcanvases built in. Resize your browser window to the large breakpoint to see the toggle for the offcanvas.

    Learn more about offcanvas navbars »

    ================================================ FILE: site/src/assets/examples/navbars-offcanvas/navbars-offcanvas.css ================================================ body { padding-bottom: 20px; } .navbar { margin-bottom: 20px; } ================================================ FILE: site/src/assets/examples/offcanvas-navbar/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Offcanvas navbar template' export const extra_css = ['offcanvas-navbar.css'] export const extra_js = [{ src: 'offcanvas-navbar.js' }] export const body_class = 'bg-body-tertiary' export const aliases = '/docs/[[config:docs_version]]/examples/offcanvas/' import Placeholder from "@shortcodes/Placeholder.astro" ---

    Bootstrap

    Since 2011
    Recent updates

    @username Some representative placeholder content, with some information about this user. Imagine this being some sort of status update, perhaps?

    @username Some more representative placeholder content, related to this other user. Another status update, perhaps.

    @username This user also gets some representative placeholder content. Maybe they did something interesting, and you really want to highlight this in the recent updates.

    All updates
    Suggestions
    Full Name Follow
    @username
    Full Name Follow
    @username
    Full Name Follow
    @username
    All suggestions
    ================================================ FILE: site/src/assets/examples/offcanvas-navbar/offcanvas-navbar.css ================================================ html, body { overflow-x: hidden; /* Prevent scroll on narrow devices */ } body { padding-top: 56px; } @media (max-width: 991.98px) { .offcanvas-collapse { position: fixed; top: 56px; /* Height of navbar */ bottom: 0; left: 100%; width: 100%; padding-right: 1rem; padding-left: 1rem; overflow-y: auto; visibility: hidden; background-color: #343a40; transition: transform .3s ease-in-out, visibility .3s ease-in-out; } .offcanvas-collapse.open { visibility: visible; transform: translateX(-100%); } } .nav-scroller .nav { color: rgba(255, 255, 255, .75); } .nav-scroller .nav-link { padding-top: .75rem; padding-bottom: .75rem; font-size: .875rem; color: #6c757d; } .nav-scroller .nav-link:hover { color: #007bff; } .nav-scroller .active { font-weight: 500; color: #343a40; } .bg-purple { background-color: #6f42c1; } ================================================ FILE: site/src/assets/examples/offcanvas-navbar/offcanvas-navbar.js ================================================ (() => { 'use strict' document.querySelector('#navbarSideCollapse').addEventListener('click', () => { document.querySelector('.offcanvas-collapse').classList.toggle('open') }) })() ================================================ FILE: site/src/assets/examples/pricing/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Pricing example' export const extra_css = ['pricing.css'] --- Check

    Pricing

    Quickly build an effective pricing table for your potential customers with this Bootstrap example. It’s built with default Bootstrap components and utilities with little customization.

    Free

    $0/mo

    • 10 users included
    • 2 GB of storage
    • Email support
    • Help center access

    Pro

    $15/mo

    • 20 users included
    • 10 GB of storage
    • Priority email support
    • Help center access

    Enterprise

    $29/mo

    • 30 users included
    • 15 GB of storage
    • Phone and email support
    • Help center access

    Compare plans

    Free Pro Enterprise
    Public
    Private
    Permissions
    Sharing
    Unlimited members
    Extra security
    ================================================ FILE: site/src/assets/examples/pricing/pricing.css ================================================ body { background-image: linear-gradient(180deg, var(--bs-secondary-bg), var(--bs-body-bg) 100px, var(--bs-body-bg)); } .container { max-width: 960px; } .pricing-header { max-width: 700px; } ================================================ FILE: site/src/assets/examples/product/index.astro ================================================ --- export const title = 'Product example' export const extra_css = ['product.css'] ---

    Designed for engineers

    Build anything you want with Aperture

    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.

    ================================================ FILE: site/src/assets/examples/product/product.css ================================================ .container { max-width: 960px; } .icon-link > .bi { width: .75em; height: .75em; } /* * Custom translucent site header */ .site-header { background-color: rgba(0, 0, 0, .85); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); } .site-header a { color: #8e8e8e; transition: color .15s ease-in-out; } .site-header a:hover { color: #fff; text-decoration: none; } /* * Dummy devices (replace them with your own or something else entirely!) */ .product-device { position: absolute; right: 10%; bottom: -30%; width: 300px; height: 540px; background-color: #333; border-radius: 21px; transform: rotate(30deg); } .product-device::before { position: absolute; top: 10%; right: 10px; bottom: 10%; left: 10px; content: ""; background-color: rgba(255, 255, 255, .1); border-radius: 5px; } .product-device-2 { top: -25%; right: auto; bottom: 0; left: 5%; background-color: #e5e5e5; } /* * Extra utilities */ .flex-equal > * { flex: 1; } @media (min-width: 768px) { .flex-md-equal > * { flex: 1; } } ================================================ FILE: site/src/assets/examples/sidebars/index.astro ================================================ --- export const title = 'Sidebars' export const extra_css = ['sidebars.css'] export const extra_js = [{src: 'sidebars.js'}] --- Bootstrap

    Sidebars examples

    ================================================ FILE: site/src/assets/examples/sidebars/sidebars.css ================================================ body { min-height: 100vh; min-height: -webkit-fill-available; } html { height: -webkit-fill-available; } main { height: 100vh; height: -webkit-fill-available; max-height: 100vh; overflow-x: auto; overflow-y: hidden; } .dropdown-toggle { outline: 0; } .btn-toggle { padding: .25rem .5rem; font-weight: 600; color: var(--bs-emphasis-color); background-color: transparent; } .btn-toggle:hover, .btn-toggle:focus { color: rgba(var(--bs-emphasis-color-rgb), .85); background-color: var(--bs-tertiary-bg); } .btn-toggle::before { width: 1.25em; line-height: 0; content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); transition: transform .35s ease; transform-origin: .5em 50%; } [data-bs-theme="dark"] .btn-toggle::before { content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28255,255,255,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); } .btn-toggle[aria-expanded="true"] { color: rgba(var(--bs-emphasis-color-rgb), .85); } .btn-toggle[aria-expanded="true"]::before { transform: rotate(90deg); } .btn-toggle-nav a { padding: .1875rem .5rem; margin-top: .125rem; margin-left: 1.25rem; } .btn-toggle-nav a:hover, .btn-toggle-nav a:focus { background-color: var(--bs-tertiary-bg); } .scrollarea { overflow-y: auto; } ================================================ FILE: site/src/assets/examples/sidebars/sidebars.js ================================================ /* global bootstrap: false */ (() => { 'use strict' const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]')) tooltipTriggerList.forEach(tooltipTriggerEl => { new bootstrap.Tooltip(tooltipTriggerEl) }) })() ================================================ FILE: site/src/assets/examples/sign-in/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Signin Template' export const extra_css = ['sign-in.css'] export const body_class = 'd-flex align-items-center py-4 bg-body-tertiary' ---

    Please sign in

    © 2017–{new Date().getFullYear()}

    ================================================ FILE: site/src/assets/examples/sign-in/sign-in.css ================================================ html, body { height: 100%; } .form-signin { max-width: 330px; padding: 1rem; } .form-signin .form-floating:focus-within { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } ================================================ FILE: site/src/assets/examples/starter-template/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Starter Template' --- 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 go 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 · © {new Date().getFullYear()}
    ================================================ FILE: site/src/assets/examples/sticky-footer/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Sticky Footer Template' export const extra_css = ['sticky-footer.css'] export const html_class = 'h-100' export const body_class = 'd-flex flex-column h-100' ---

    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.
    ================================================ FILE: site/src/assets/examples/sticky-footer/sticky-footer.css ================================================ /* Custom page CSS -------------------------------------------------- */ /* Not required for template or sticky footer method. */ .container { width: auto; max-width: 680px; padding: 0 15px; } ================================================ FILE: site/src/assets/examples/sticky-footer-navbar/index.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' export const title = 'Sticky Footer Navbar Template' export const extra_css = ['sticky-footer-navbar.css'] export const html_class = 'h-100' export const body_class = 'd-flex flex-column h-100' ---

    Sticky footer with fixed navbar

    Pin a footer to the bottom of the viewport in desktop browsers with this custom HTML and CSS. A fixed navbar has been added with padding-top: 60px; on the main > .container.

    Back to the default sticky footer minus the navbar.

    Place sticky footer content here.
    ================================================ FILE: site/src/assets/examples/sticky-footer-navbar/sticky-footer-navbar.css ================================================ /* Custom page CSS -------------------------------------------------- */ /* Not required for template or sticky footer method. */ main > .container { padding: 60px 15px 0; } ================================================ FILE: site/src/assets/partials/sidebar.js ================================================ // NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT // IT'S ALL JUST JUNK FOR OUR DOCS! // ++++++++++++++++++++++++++++++++++++++++++ /* * JavaScript for Bootstrap's docs (https://getbootstrap.com/) * Copyright 2011-2026 The Bootstrap Authors * Licensed under the Creative Commons Attribution 3.0 Unported License. * For details, see https://creativecommons.org/licenses/by/3.0/. */ export default () => { // Scroll the active sidebar link into view const sidenav = document.querySelector('.bd-sidebar') const sidenavActiveLink = document.querySelector('.bd-links-nav .active') if (!sidenav || !sidenavActiveLink) { return } const sidenavHeight = sidenav.clientHeight const sidenavActiveLinkTop = sidenavActiveLink.offsetTop const sidenavActiveLinkHeight = sidenavActiveLink.clientHeight const viewportTop = sidenavActiveLinkTop const viewportBottom = viewportTop - sidenavHeight + sidenavActiveLinkHeight if (sidenav.scrollTop > viewportTop || sidenav.scrollTop < viewportBottom) { sidenav.scrollTop = viewportTop - (sidenavHeight / 2) + (sidenavActiveLinkHeight / 2) } } ================================================ FILE: site/src/assets/partials/snippets.js ================================================ // NOTICE!!! Initially embedded in our docs this JavaScript // file contains elements that can help you create reproducible // use cases in StackBlitz for instance. // In a real project please adapt this content to your needs. // ++++++++++++++++++++++++++++++++++++++++++ /* * JavaScript for Bootstrap's docs (https://getbootstrap.com/) * Copyright 2011-2026 The Bootstrap Authors * Licensed under the Creative Commons Attribution 3.0 Unported License. * For details, see https://creativecommons.org/licenses/by/3.0/. */ /* global bootstrap: false */ export default () => { // -------- // Tooltips // -------- // Instantiate all tooltips in a docs or StackBlitz document.querySelectorAll('[data-bs-toggle="tooltip"]') .forEach(tooltip => { new bootstrap.Tooltip(tooltip) }) // -------- // Popovers // -------- // Instantiate all popovers in docs or StackBlitz document.querySelectorAll('[data-bs-toggle="popover"]') .forEach(popover => { new bootstrap.Popover(popover) }) // ------------------------------- // Toasts // ------------------------------- // Used by 'Placement' example in docs or StackBlitz const toastPlacement = document.getElementById('toastPlacement') if (toastPlacement) { document.getElementById('selectToastPlacement').addEventListener('change', function () { if (!toastPlacement.dataset.originalClass) { toastPlacement.dataset.originalClass = toastPlacement.className } toastPlacement.className = `${toastPlacement.dataset.originalClass} ${this.value}` }) } // Instantiate all toasts in docs pages only document.querySelectorAll('.bd-example .toast') .forEach(toastNode => { const toast = new bootstrap.Toast(toastNode, { autohide: false }) toast.show() }) // Instantiate all toasts in docs pages only // js-docs-start live-toast const toastTrigger = document.getElementById('liveToastBtn') const toastLiveExample = document.getElementById('liveToast') if (toastTrigger) { const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample) toastTrigger.addEventListener('click', () => { toastBootstrap.show() }) } // js-docs-end live-toast // ------------------------------- // Alerts // ------------------------------- // Used in 'Show live alert' example in docs or StackBlitz // js-docs-start live-alert const alertPlaceholder = document.getElementById('liveAlertPlaceholder') const appendAlert = (message, type) => { const wrapper = document.createElement('div') wrapper.innerHTML = [ `' ].join('') alertPlaceholder.append(wrapper) } const alertTrigger = document.getElementById('liveAlertBtn') if (alertTrigger) { alertTrigger.addEventListener('click', () => { appendAlert('Nice, you triggered this alert message!', 'success') }) } // js-docs-end live-alert // -------- // Carousels // -------- // Instantiate all non-autoplaying carousels in docs or StackBlitz document.querySelectorAll('.carousel:not([data-bs-ride="carousel"])') .forEach(carousel => { bootstrap.Carousel.getOrCreateInstance(carousel) }) // ------------------------------- // Checks & Radios // ------------------------------- // Indeterminate checkbox example in docs and StackBlitz document.querySelectorAll('.bd-example-indeterminate [type="checkbox"]') .forEach(checkbox => { if (checkbox.id.includes('Indeterminate')) { checkbox.indeterminate = true } }) // ------------------------------- // Links // ------------------------------- // Disable empty links in docs examples only document.querySelectorAll('.bd-content [href="#"]') .forEach(link => { link.addEventListener('click', event => { event.preventDefault() }) }) // ------------------------------- // Modal // ------------------------------- // Modal 'Varying modal content' example in docs and StackBlitz // js-docs-start varying-modal-content const exampleModal = document.getElementById('exampleModal') if (exampleModal) { exampleModal.addEventListener('show.bs.modal', event => { // Button that triggered the modal const button = event.relatedTarget // Extract info from data-bs-* attributes const recipient = button.getAttribute('data-bs-whatever') // If necessary, you could initiate an Ajax request here // and then do the updating in a callback. // Update the modal's content. const modalTitle = exampleModal.querySelector('.modal-title') const modalBodyInput = exampleModal.querySelector('.modal-body input') modalTitle.textContent = `New message to ${recipient}` modalBodyInput.value = recipient }) } // js-docs-end varying-modal-content // ------------------------------- // Offcanvas // ------------------------------- // 'Offcanvas components' example in docs only const myOffcanvas = document.querySelectorAll('.bd-example-offcanvas .offcanvas') if (myOffcanvas) { myOffcanvas.forEach(offcanvas => { offcanvas.addEventListener('show.bs.offcanvas', event => { event.preventDefault() }, false) }) } } ================================================ FILE: site/src/assets/search.js ================================================ // NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT // IT'S ALL JUST JUNK FOR OUR DOCS! // ++++++++++++++++++++++++++++++++++++++++++ /*! * JavaScript for Bootstrap's docs (https://getbootstrap.com/) * Copyright 2024-2026 The Bootstrap Authors * Licensed under the Creative Commons Attribution 3.0 Unported License. * For details, see https://creativecommons.org/licenses/by/3.0/. */ import docsearch from '@docsearch/js' (() => { // These values will be replaced by Astro's Vite plugin const CONFIG = { apiKey: '__API_KEY__', indexName: '__INDEX_NAME__', appId: '__APP_ID__' } const searchElement = document.getElementById('docsearch') if (!searchElement) { return } const siteDocsVersion = searchElement.getAttribute('data-bd-docs-version') docsearch({ apiKey: CONFIG.apiKey, indexName: CONFIG.indexName, appId: CONFIG.appId, container: searchElement, searchParameters: { facetFilters: [`version:${siteDocsVersion}`] }, transformItems(items) { return items.map(item => { const liveUrl = 'https://getbootstrap.com/' item.url = window.location.origin.startsWith(liveUrl) ? // On production, return the result as is item.url : // On development or Netlify, replace `item.url` with a trailing slash, // so that the result link is relative to the server root item.url.replace(liveUrl, '/') // Prevent jumping to first header if (item.anchor === 'content') { item.url = item.url.replace(/#content$/, '') item.anchor = null } return item }) } }) })() ================================================ FILE: site/src/assets/snippets.js ================================================ /* * JavaScript for Bootstrap's docs (https://getbootstrap.com/) * Copyright 2011-2026 The Bootstrap Authors * Licensed under the Creative Commons Attribution 3.0 Unported License. * For details, see https://creativecommons.org/licenses/by/3.0/. */ // Note that this file is not published; we only include it in scripts.html // for StackBlitz to work /* eslint-disable import/no-unresolved */ import snippets from 'js/partials/snippets.js' /* eslint-enable import/no-unresolved */ snippets() ================================================ FILE: site/src/assets/stackblitz.js ================================================ // NOTICE!!! Initially embedded in our docs this JavaScript // file contains elements that can help you create reproducible // use cases in StackBlitz for instance. // In a real project please adapt this content to your needs. // ++++++++++++++++++++++++++++++++++++++++++ /*! * JavaScript for Bootstrap's docs (https://getbootstrap.com/) * Copyright 2024-2026 The Bootstrap Authors * Licensed under the Creative Commons Attribution 3.0 Unported License. * For details, see https://creativecommons.org/licenses/by/3.0/. */ import sdk from '@stackblitz/sdk' // eslint-disable-next-line import/no-unresolved import snippetsContent from './partials/snippets.js?raw' // These values will be replaced by Astro's Vite plugin const CONFIG = { cssCdn: '__CSS_CDN__', jsBundleCdn: '__JS_BUNDLE_CDN__', docsVersion: '__DOCS_VERSION__' } // Open in StackBlitz logic document.querySelectorAll('.btn-edit').forEach(btn => { btn.addEventListener('click', event => { const codeSnippet = event.target.closest('.bd-code-snippet') const exampleEl = codeSnippet.querySelector('.bd-example') const htmlSnippet = exampleEl.innerHTML const jsSnippet = codeSnippet.querySelector('.btn-edit').getAttribute('data-sb-js-snippet') // Get extra classes for this example const classes = Array.from(exampleEl.classList).join(' ') openBootstrapSnippet(htmlSnippet, jsSnippet, classes) }) }) const openBootstrapSnippet = (htmlSnippet, jsSnippet, classes) => { const indexHtml = ` Bootstrap Example ${htmlSnippet.trimStart().replace(/^/gm, ' ').replace(/^ {4}$/gm, '').trimEnd()} ` // Modify the snippets content to convert export default to a variable and invoke it let modifiedSnippetsContent = '' if (jsSnippet) { // Replace export default with a variable assignment modifiedSnippetsContent = snippetsContent.replace( 'export default () => {', 'const snippets_default = () => {' ) // Add IIFE wrapper and execution modifiedSnippetsContent = `(() => { ${modifiedSnippetsContent} // snippets_default(); })();` } const project = { files: { 'index.html': indexHtml, ...(jsSnippet && { 'index.js': modifiedSnippetsContent }) }, title: 'Bootstrap Example', description: `Official example from ${window.location.href}`, template: jsSnippet ? 'javascript' : 'html', tags: ['bootstrap'] } sdk.openProject(project, { openFile: 'index.html' }) } ================================================ FILE: site/src/components/Ads.astro ================================================ --- --- ================================================ FILE: site/src/components/DocsScripts.astro ================================================ --- --- ================================================ FILE: site/src/components/DocsSidebar.astro ================================================ --- import { getData } from '@libs/data' import { getConfig } from '@libs/config' import { docsPages } from '@libs/content' import { getSlug } from '@libs/utils' const sidebar = getData('sidebar') --- ================================================ FILE: site/src/components/Scripts.astro ================================================ --- import { getVersionedBsJsProps } from '@libs/bootstrap' import type { Layout } from '@libs/layout' import DocsScripts from './DocsScripts.astro' interface Props { layout: Layout } const { layout } = Astro.props --- {layout === 'docs' && } ================================================ FILE: site/src/components/TableOfContents.astro ================================================ --- import type { MarkdownHeading } from 'astro' import { generateToc, type TocEntry } from '@libs/toc' interface Props { headings?: MarkdownHeading[] entries?: TocEntry[] } const { entries, headings } = Astro.props const toc = entries ? entries : generateToc(headings ?? []) ---
      { toc.map(({ children, slug, text }) => { return (
    • {text} {children.length > 0 && }
    • ) }) }
    ================================================ FILE: site/src/components/footer/Footer.astro ================================================ --- import BootstrapWhiteFillIcon from '@components/icons/BootstrapWhiteFillIcon.astro' import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' --- ================================================ FILE: site/src/components/head/Analytics.astro ================================================ --- import { getConfig } from '@libs/config' --- ================================================ FILE: site/src/components/head/Favicons.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' --- ================================================ FILE: site/src/components/head/Head.astro ================================================ --- import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' import type { Layout } from '@libs/layout' import Stylesheet from '@components/head/Stylesheet.astro' import Favicons from '@components/head/Favicons.astro' import Social from '@components/head/Social.astro' import Analytics from '@components/head/Analytics.astro' interface Props { description: string direction?: 'rtl' layout: Layout robots: string | undefined thumbnail: string title: string } const { description, direction, layout, robots, thumbnail, title } = Astro.props const canonicalUrl = new URL(Astro.url.pathname, Astro.site) const isHome = Astro.url.pathname === '/' const pageTitle = isHome ? `${getConfig().title} · ${getConfig().subtitle}` : `${title} · ${getConfig().title} v${getConfig().docs_version}` // Dynamic imports to avoid build-time processing const Scss = import.meta.env.PROD ? null : await import('@components/head/Scss.astro') const ScssProd = import.meta.env.PROD ? await import('@components/head/ScssProd.astro') : null --- {pageTitle} {robots && } {import.meta.env.PROD && ScssProd && ( )} {!import.meta.env.PROD && Scss && ( )} ================================================ FILE: site/src/components/head/Scss.astro ================================================ --- --- ================================================ FILE: site/src/components/head/ScssProd.astro ================================================ --- --- ================================================ FILE: site/src/components/head/Social.astro ================================================ --- import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' import { getStaticImageSize } from '@libs/image' import type { Layout } from '@libs/layout' interface Props { description: string layout: Layout thumbnail: string title: string } const { description, layout, thumbnail, title } = Astro.props const socialImageUrl = new URL(getVersionedDocsPath(`assets/${thumbnail}`), Astro.site) const socialImageSize = await getStaticImageSize(`/docs/[version]/assets/${thumbnail}`) --- ================================================ FILE: site/src/components/head/Stylesheet.astro ================================================ --- import { getVersionedBsCssProps } from '@libs/bootstrap' import type { Layout } from '@libs/layout' interface Props { direction?: 'rtl' layout: Layout } const { direction } = Astro.props --- ================================================ FILE: site/src/components/header/Header.astro ================================================ --- import type { CollectionEntry } from 'astro:content' import type { Layout } from '@libs/layout' import Skippy from '@components/header/Skippy.astro' import Symbols from '@components/icons/Symbols.astro' import Navigation from '@components/header/Navigation.astro' interface Props { addedIn?: CollectionEntry<'docs'>['data']['added'] layout: Layout title: string } const { addedIn, layout, title } = Astro.props --- ================================================ FILE: site/src/components/header/LinkItem.astro ================================================ --- interface Props { active?: boolean class?: string href: string rel?: HTMLAnchorElement['rel'] target?: HTMLAnchorElement['target'] track?: boolean } const { active, class: className, track, ...props } = Astro.props const content = await Astro.slots.render('default') --- ================================================ FILE: site/src/components/header/Navigation.astro ================================================ --- import type { CollectionEntry } from 'astro:content' import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' import type { Layout } from '@libs/layout' import BootstrapWhiteFillIcon from '@components/icons/BootstrapWhiteFillIcon.astro' import GitHubIcon from '@components/icons/GitHubIcon.astro' import HamburgerIcon from '@components/icons/HamburgerIcon.astro' import LinkItem from '@components/header/LinkItem.astro' import OpenCollectiveIcon from '@components/icons/OpenCollectiveIcon.astro' import XIcon from '@components/icons/XIcon.astro' import Versions from '@components/header/Versions.astro' import ThemeToggler from '@layouts/partials/ThemeToggler.astro' interface Props { addedIn?: CollectionEntry<'docs'>['data']['added'] layout: Layout title: string } const { addedIn, layout, title } = Astro.props --- ================================================ FILE: site/src/components/header/Skippy.astro ================================================ --- import type { Layout } from '@libs/layout' interface Props { layout: Layout } const { layout } = Astro.props ---
    Skip to main content { layout === 'docs' && ( Skip to docs navigation ) }
    ================================================ FILE: site/src/components/header/Versions.astro ================================================ --- import type { CollectionEntry } from 'astro:content' import { getConfig } from '@libs/config' import type { Layout } from '@libs/layout' import { getVersionedDocsPath } from '@libs/path' interface Props { addedIn?: CollectionEntry<'docs'>['data']['added'] layout: Layout } const { addedIn, layout } = Astro.props const { slug, version } = Astro.params const isHome = Astro.url.pathname === '/' let versionsLink = '' if (layout === 'docs' && version === getConfig().docs_version) { versionsLink = `${slug}/` } else if (layout === 'single' && Astro.url.pathname.startsWith(getVersionedDocsPath(''))) { versionsLink = Astro.url.pathname.replace(getVersionedDocsPath(''), '') } const addedIn51 = addedIn?.version === '5.1' const addedIn52 = addedIn?.version === '5.2' const addedIn53 = addedIn?.version === '5.3' --- ================================================ FILE: site/src/components/home/CSSVariables.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' import Code from '@shortcodes/Code.astro' ---

    Build and extend in real-time with CSS variables

    Bootstrap 5 is evolving with each release to better utilize CSS variables for global theme styles, individual components, and even utilities. We provide dozens of variables for colors, font styles, and more at a :root level for use anywhere. On components and utilities, CSS variables are scoped to the relevant class and can easily be modified.

    Learn more about CSS variables

    Using CSS variables

    Use any of our global :root variables to write new styles. CSS variables use the var(--bs-variableName) syntax and can be inherited by children elements.

    Customizing via CSS variables

    Override global, component, or utility class variables to customize Bootstrap just how you like. No need to redeclare each rule, just a new variable value.

    ================================================ FILE: site/src/components/home/ComponentUtilities.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' import Code from '@shortcodes/Code.astro' ---

    Components, meet the Utility API

    New in Bootstrap 5, our utilities are now generated by our Utility API. We built it as a feature-packed Sass map that can be quickly and easily customized. It's never been easier to add, remove, or modify any utility classes. Make utilities responsive, add pseudo-class variants, and give them custom names.

    Quickly customize components

    Apply any of our included utility classes to our components to customize their appearance, like the navigation example below. There are hundreds of classes available—from positioning and sizing to colors and effects. Mix them with CSS variable overrides for even more control.

    `} lang="html" />

    Explore customized components

    Create and extend utilities

    Use Bootstrap's utility API to modify any of our included utilities or create your own custom utilities for any project. Import Bootstrap first, then use Sass map functions to modify, add, or remove utilities.

    Explore the utility API

    ================================================ FILE: site/src/components/home/Customize.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' import Code from '@shortcodes/Code.astro' ---

    Customize everything with Sass

    Bootstrap utilizes Sass for a modular and customizable architecture. Import only the components you need, enable global options like gradients and shadows, and write your own CSS with our variables, maps, functions, and mixins.

    Learn more about customizing

    Include all of Bootstrap’s Sass

    Import one stylesheet and you're off to the races with every feature of our CSS.

    Learn more about our global Sass options.

    Include what you need

    The easiest way to customize Bootstrap—include only the CSS you need.

    Learn more about using Bootstrap with Sass.

    ================================================ FILE: site/src/components/home/GetStarted.astro ================================================ --- import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' import Code from '@shortcodes/Code.astro' ---

    Get started any way you want

    Jump right into building with Bootstrap—use the CDN, install it via package manager, or download the source code.

    Read installation docs

    Install via package manager

    Install Bootstrap’s source Sass and JavaScript files via npm, RubyGems, Composer, or Meteor. Package-managed installs don’t include documentation or our full build scripts. You can also use any demo from our Examples repo to quickly jumpstart Bootstrap projects.

    Read our installation docs for more info and additional package managers.

    Include via CDN

    When you only need to include Bootstrap’s compiled CSS or JS, you can use jsDelivr. See it in action with our simple quick start, or browse the examples to jumpstart your next project. You can also choose to include Popper and our JS separately.

    `} lang="html" /> `} lang="html" />

    Read our getting started guides

    Get a jump on including Bootstrap's source files in a new project with our official guides.

    ================================================ FILE: site/src/components/home/Icons.astro ================================================ --- import { getConfig } from '@libs/config' import CircleSquareIcon from '@components/icons/CircleSquareIcon.astro' import ResponsiveImage from '@layouts/partials/ResponsiveImage.astro' ---

    Personalize it with Bootstrap Icons

    Bootstrap Icons is an open source SVG icon library featuring over 1,800 glyphs, with more added every release. They're designed to work in any project, whether you use Bootstrap itself or not. Use them as SVGs or icon fonts—both options give you vector scaling and easy customization via CSS.

    Get Bootstrap Icons

    ================================================ FILE: site/src/components/home/MastHead.astro ================================================ --- import { getConfig } from '@libs/config' import { getVersionedDocsPath } from '@libs/path' import Ads from '@components/Ads.astro' import Code from '@components/shortcodes/Code.astro' import ResponsiveImage from '@layouts/partials/ResponsiveImage.astro' ---
    Get Security Updates for Bootstrap 3 & 4

    Build fast, responsive sites with Bootstrap

    Powerful, extensible, and feature-packed frontend toolkit. Build and customize with Sass, utilize prebuilt grid system and components, and bring projects to life with powerful JavaScript plugins.

    Currently v{getConfig().current_version} · Download · All releases

    ================================================ FILE: site/src/components/home/Plugins.astro ================================================ --- import { getVersionedDocsPath } from '@libs/path' import { getData } from '@libs/data' import Code from '@shortcodes/Code.astro' const plugins = getData('plugins') ---

    Powerful JavaScript plugins without jQuery

    Add toggleable hidden elements, modals and offcanvas menus, popovers and tooltips, and so much more—all without jQuery. Bootstrap's JavaScript is HTML-first, meaning most plugins are added with data attributes in your HTML. Need more control? Include individual plugins programmatically.

    Learn more about Bootstrap JavaScript

    Data attribute API

    Why write more JavaScript when you can write HTML? Nearly all of Bootstrap's JavaScript plugins feature a first-class data API, allowing you to use JavaScript just by adding data attributes.

    `} lang="html" />

    Learn more about our JavaScript as modules and using the programmatic API.

    Comprehensive set of plugins

    Bootstrap features a dozen plugins that you can drop into any project. Drop them in all at once, or choose just the ones you need.


    { plugins.map((plugin) => { return ( ) }) }
    ================================================ FILE: site/src/components/icons/BootstrapWhiteFillIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- Bootstrap ================================================ FILE: site/src/components/icons/CircleSquareIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- ================================================ FILE: site/src/components/icons/DropletFillIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- ================================================ FILE: site/src/components/icons/GitHubIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- GitHub ================================================ FILE: site/src/components/icons/HamburgerIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- ================================================ FILE: site/src/components/icons/OpenCollectiveIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- Open Collective ================================================ FILE: site/src/components/icons/Symbols.astro ================================================ --- --- ================================================ FILE: site/src/components/icons/XIcon.astro ================================================ --- import type { SvgIconProps } from '@libs/icon' type Props = SvgIconProps const { class: className, height, width } = Astro.props --- X ================================================ FILE: site/src/components/shortcodes/AddedIn.astro ================================================ --- /* * Outputs badge to identify the first version something was added */ interface Props { version: string } const { version } = Astro.props --- Added in v{version} ================================================ FILE: site/src/components/shortcodes/BsTable.astro ================================================ --- interface Props { /** * The CSS class to apply to the table. * Note that the prop is not used in this component, but in a rehype plugin applying the classes to the table element * directly on the HTML AST (HAST) generated by Astro. * @default "table" * @see src/libs/rehype.ts */ class?: string } ---
    ================================================ FILE: site/src/components/shortcodes/Callout.astro ================================================ --- import { getCalloutByName } from '@libs/content' import type { MarkdownInstance } from 'astro' interface Props { /** * The name of an existing callout to display located in `src/content/callouts`. * This will override any content passed in via the default slot. */ name?: | 'danger-async-methods' | 'info-mediaqueries-breakpoints' | 'info-npm-starter' | 'info-prefersreducedmotion' | 'info-sanitizer' | 'warning-color-assistive-technologies' | 'warning-data-bs-title-vs-title' | 'warning-input-support' /** * The type of callout to display. One of `info`, `danger`, or `warning`. * @default 'info' */ type?: 'danger' | 'info' | 'warning' } const { name, type = 'info' } = Astro.props let Content: MarkdownInstance<{}>['Content'] | undefined if (name) { const callout = await getCalloutByName(name) if (!callout) { throw new Error(`Could not find callout with name '${name}'.`) } const namedCallout = await callout.render() Content = namedCallout.Content } ---
    {Content ? : }
    ================================================ FILE: site/src/components/shortcodes/CalloutDeprecatedDarkVariants.astro ================================================ --- /* * Outputs message about dark mode component variants being deprecated in v5.3. */ interface Props { component: string } const { component } = Astro.props ---

    Heads up! Dark variants for components were deprecated in v5.3.0 with the introduction of color modes. Instead of adding .{component}-dark, set data-bs-theme="dark" on the root element, a parent wrapper, or the component itself.

    ================================================ FILE: site/src/components/shortcodes/Code.astro ================================================ --- import fs from 'node:fs' import path from 'node:path' import { Prism } from '@astrojs/prism' interface Props { /** * The CSS class(es) to be added to the `pre` HTML element when rendering code blocks in Markdown. * Note that this prop is not used when the component is invoked directly. */ class?: string /** * The code to highlight. * If an array is passed, elements will be joined with a new line. */ code?: string | string[] /** * The CSS class(es) to be added to the `div` wrapper HTML element. */ containerClass?: string /** * The language to use for highlighting. * @see https://prismjs.com/#supported-languages */ lang?: string /** * If the `filePath` prop is defined, this prop can be used to specify a regex containing a match group to extract * only a part of the file. */ fileMatch?: string /** * A path to the file containing the code to highlight relative to the root of the repository. * This takes precedence over the `code` prop. */ filePath?: string /** * Defines if the `` component is nested inside an `` component or not. * @default false */ nestedInExample?: boolean } const { class: className, code, containerClass, fileMatch, filePath, lang, nestedInExample = false } = Astro.props let codeToDisplay = filePath ? fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') : Array.isArray(code) ? code.join('\n') : code if (filePath && fileMatch && codeToDisplay) { const match = codeToDisplay.match(new RegExp(fileMatch)) if (!match || !match[0]) { throw new Error(`The file at ${filePath} does not contains a match for the regex '${fileMatch}'.`) } codeToDisplay = match[0] } ---
    { nestedInExample ? (<>) : Astro.slots.has('pre') ? ( ) : (
    ) }
    { codeToDisplay && lang ? ( ) : ( /* prettier-ignore */
    ) }
    ================================================ FILE: site/src/components/shortcodes/DeprecatedIn.astro ================================================ --- /* * Outputs badge to identify the version something was deprecated */ interface Props { version: string } const { version } = Astro.props --- Deprecated in v{version} ================================================ FILE: site/src/components/shortcodes/Example.astro ================================================ --- import { replacePlaceholdersInHtml } from '@libs/placeholder' import Code from '@components/shortcodes/Code.astro' interface Props { /** * Defines if extra JS snippet should be added to StackBlitz or not. * @default false */ addStackblitzJs?: boolean /** * The example code. * If an array is passed, elements will be joined with a new line. */ code: string | string[] /** * The CSS class(es) to be added to the preview wrapping `div` element. */ class?: string /** * The preview wrapping `div` element ID. */ id?: string /** * Language used to display the code. * @default 'html' */ lang?: string /** * Defines if the markup should be visible or not. * @default true */ showMarkup?: boolean /** * Defines if the preview should be visible or not. * @default true */ showPreview?: boolean } const { addStackblitzJs = false, code, class: className, id, lang = 'html', showMarkup = true, showPreview = true } = Astro.props let markup = Array.isArray(code) ? code.join('\n') : code markup = replacePlaceholdersInHtml(markup) const simplifiedMarkup = markup .replace( //g, (match, classes) => `...` ) .replace( //g, (match, classes) => `...` ) ---
    { showPreview && (
    ) } { showMarkup && ( <> {showPreview && (
    {lang}
    )} ) }
    ================================================ FILE: site/src/components/shortcodes/GuideFooter.mdx ================================================
    _See something wrong or out of date here? Please [open an issue on GitHub]([[config:repo]]/issues/new/choose). Need help troubleshooting? [Search or start a discussion]([[config:repo]]/discussions) on GitHub._ ================================================ FILE: site/src/components/shortcodes/JsDataAttributes.mdx ================================================ As options can be passed via data attributes or JavaScript, you can append an option name to `data-bs-`, as in `data-bs-animation="{value}"`. Make sure to change the case type of the option name from “_camelCase_” to “_kebab-case_” when passing the options via data attributes. For example, use `data-bs-custom-class="beautifier"` instead of `data-bs-customClass="beautifier"`. As of Bootstrap 5.2.0, all components support an **experimental** reserved data attribute `data-bs-config` that can house simple component configuration as a JSON string. When an element has `data-bs-config='{"delay":0, "title":123}'` and `data-bs-title="456"` attributes, the final `title` value will be `456` and the separate data attributes will override values given on `data-bs-config`. In addition, existing data attributes are able to house JSON values like `data-bs-delay='{"show":0,"hide":150}'`. The final configuration object is the merged result of `data-bs-config`, `data-bs-`, and `js object` where the latest given key-value overrides the others. ================================================ FILE: site/src/components/shortcodes/JsDismiss.astro ================================================ --- import Code from '@shortcodes/Code.astro' interface Props { name: string } const { name } = Astro.props ---

    Dismissal can be achieved with the data-bs-dismiss attribute on a button within the {name} as demonstrated below:

    `} lang="html" />

    or on a button outside the {name} using the additional data-bs-target as demonstrated below:

    `} lang="html" /> ================================================ FILE: site/src/components/shortcodes/JsDocs.astro ================================================ --- import fs from 'node:fs' import { getConfig } from '@libs/config' import Code from '@shortcodes/Code.astro' // Prints everything between `// js-docs-start "name"` and `// js-docs-end "name"` // comments in the docs. interface Props { /** * Reference name used to find the content to display within the content of the `file` prop. */ name: string /** * File path that contains the content to display relative to the root of the repository. */ file: string } const { name, file } = Astro.props if (!name || !file) { throw new Error( `Missing required parameter(s) for the '' component, expected both 'name' and 'file' but got 'name: ${name}' and 'file: ${file}'.` ) } let content: string try { const fileContent = fs.readFileSync(file, 'utf8') const matches = fileContent.match(new RegExp(`\/\/ js-docs-start ${name}\n((?:.|\n)*)\/\/ js-docs-end ${name}`, 'm')) if (!matches || !matches[1]) { throw new Error( `Failed to find the content named '${name}', make sure that '// js-docs-start ${name}' and '// js-docs-end ${name}' are defined.` ) } content = matches[1] // Fix the indentation by removing extra spaces at the beginning of each line const lines = content.split('\n') const spaceCounts = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^ */)[0].length) const minSpaces = spaceCounts.length ? Math.min(...spaceCounts) : 0 content = lines.map((line) => line.slice(minSpaces)).join('\n') } catch (error) { throw new Error(`Failed to find the content to render in the '' component at '${file}'.`, { cause: error }) } --- ================================================ FILE: site/src/components/shortcodes/Placeholder.astro ================================================ --- import { getPlaceholder, type PlaceholderOptions } from '@libs/placeholder' type Props = Partial const { options: { background, color, showText, showTitle, text, title }, props, type } = getPlaceholder(Astro.props) --- { type === 'img' ? ( ) : ( {showTitle && {title}} {showText && ( {text} )} ) } ================================================ FILE: site/src/components/shortcodes/ScssDocs.astro ================================================ --- import fs from 'node:fs' import { getConfig } from '@libs/config' import Code from '@shortcodes/Code.astro' // Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"` // comments in the docs. interface Props { /** * Reference name used to find the content to display within the content of the `file` prop. */ name: string /** * File path that contains the content to display relative to the root of the repository. */ file: string } const { name, file } = Astro.props if (!name || !file) { throw new Error( `Missing required parameter(s) for the '' component, expected both 'name' and 'file' but got 'name: ${name}' and 'file: ${file}'.` ) } let content: string try { const fileContent = fs.readFileSync(file, 'utf8') const matches = fileContent.match( new RegExp(`\/\/ scss-docs-start ${name}\n((?:.|\n)*)\/\/ scss-docs-end ${name}`, 'm') ) if (!matches || !matches[1]) { throw new Error( `Failed to find the content named '${name}', make sure that '// scss-docs-start ${name}' and '// scss-docs-end ${name}' are defined.` ) } content = matches[1].replaceAll(' !default', '') // Fix the indentation by removing extra spaces at the beginning of each line const lines = content.split('\n') const spaceCounts = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^ */)[0].length) const minSpaces = spaceCounts.length ? Math.min(...spaceCounts) : 0 content = lines.map((line) => line.slice(minSpaces)).join('\n') } catch (error) { throw new Error(`Failed to find the content to render in the '' component at '${file}'.`, { cause: error }) } --- ================================================ FILE: site/src/components/shortcodes/Table.astro ================================================ --- import Code from '@shortcodes/Code.astro' import Example from '@shortcodes/Example.astro' import * as tableContent from '@shortcodes/TableContent.md' interface Props { /** * Any class(es) to be added to the `` element (both in the example and code snippet). */ class?: string /** * Show a simplified version in the example code snippet by replacing the table content inside `
    ` & `
    ` * with `...`. * @default true */ simplified?: boolean } const { class: className, simplified = true } = Astro.props const tableCode = ` ${simplified ? ' ...' : await tableContent.compiledContent()} ` const exampleCode = ` ${await tableContent.compiledContent()} ` --- ================================================ FILE: site/src/components/shortcodes/TableContent.md ================================================ # First Last Handle 1 Mark Otto @mdo 2 Jacob Thornton @fat 3 John Doe @social ================================================ FILE: site/src/content/callouts/danger-async-methods.md ================================================ **All API methods are asynchronous and start a transition.** They return to the caller as soon as the transition is started, but before it ends. In addition, a method call on a transitioning component will be ignored. [Learn more in our JavaScript docs.](/docs/[[config:docs_version]]/getting-started/javascript/#asynchronous-functions-and-transitions) ================================================ FILE: site/src/content/callouts/info-mediaqueries-breakpoints.md ================================================ **Why subtract .02px?** Browsers don’t currently support [range context queries](https://www.w3.org/TR/mediaqueries-4/#range-context), so we work around the limitations of [`min-` and `max-` prefixes](https://www.w3.org/TR/mediaqueries-4/#mq-min-max) and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision. ================================================ FILE: site/src/content/callouts/info-npm-starter.md ================================================ **Get started with Bootstrap via npm with our starter project!** Head to the [Sass & JS example](https://github.com/twbs/examples/tree/main/sass-js) template repository to see how to build and customize Bootstrap in your own npm project. Includes Sass compiler, Autoprefixer, Stylelint, PurgeCSS, and Bootstrap Icons. ================================================ FILE: site/src/content/callouts/info-prefersreducedmotion.md ================================================ The animation effect of this component is dependent on the `prefers-reduced-motion` media query. See the [reduced motion section of our accessibility documentation](/docs/[[config:docs_version]]/getting-started/accessibility/#reduced-motion). ================================================ FILE: site/src/content/callouts/info-sanitizer.md ================================================ By default, this component uses the built-in content sanitizer, which strips out any HTML elements that are not explicitly allowed. See the [sanitizer section in our JavaScript documentation](/docs/[[config:docs_version]]/getting-started/javascript/#sanitizer) for more details. ================================================ FILE: site/src/content/callouts/warning-color-assistive-technologies.md ================================================ **Accessibility tip:** Using color to add meaning only provides a visual indication, which will not be conveyed to users of assistive technologies like screen readers. Please ensure the meaning is obvious from the content itself (e.g., the visible text with a [_sufficient_ color contrast](/docs/[[config:docs_version]]/getting-started/accessibility/#color-contrast)) or is included through alternative means, such as additional text hidden with the `.visually-hidden` class. ================================================ FILE: site/src/content/callouts/warning-data-bs-title-vs-title.md ================================================ Feel free to use either `title` or `data-bs-title` in your HTML. When `title` is used, Popper will replace it automatically with `data-bs-title` when the element is rendered. ================================================ FILE: site/src/content/callouts/warning-input-support.md ================================================ Some date inputs types are [not fully supported](https://caniuse.com/input-datetime) by the latest versions of Safari and Firefox. ================================================ FILE: site/src/content/config.ts ================================================ import { z, defineCollection } from 'astro:content' const docsSchema = z.object({ added: z .object({ show_badge: z.boolean().optional(), version: z.string() }) .optional(), aliases: z.string().or(z.string().array()).optional(), description: z.string(), direction: z.literal('rtl').optional(), extra_js: z .object({ async: z.boolean().optional(), src: z.string() }) .array() .optional(), sections: z .object({ description: z.string(), title: z.string() }) .array() .optional(), thumbnail: z.string().optional(), title: z.string(), toc: z.boolean().optional() }) const docsCollection = defineCollection({ schema: docsSchema }) const calloutsSchema = z.object({}) const calloutsCollection = defineCollection({ schema: calloutsSchema }) export const collections = { docs: docsCollection, callouts: calloutsCollection } ================================================ FILE: site/src/content/docs/about/brand.mdx ================================================ --- title: Brand guidelines description: Documentation and examples for Bootstrap’s logo and brand usage guidelines. toc: true --- Have a need for Bootstrap’s brand resources? Great! We have only a few guidelines we follow, and in turn ask you to follow as well. ## Logo When referencing Bootstrap, use our logo mark. Do not modify our logos in any way. Do not use Bootstrap’s branding for your own open or closed source projects.
    Bootstrap
    Our logo mark is also available in black and white. All rules for our primary logo apply to these as well.
    Bootstrap
    Bootstrap
    ## Name Bootstrap should always be referred to as just **Bootstrap**. No capital _s_.
    Bootstrap
    Correct
    BootStrap
    Incorrect
    ================================================ FILE: site/src/content/docs/about/license.mdx ================================================ --- title: License FAQs description: Commonly asked questions about Bootstrap’s open source license. --- Bootstrap is released under the MIT license and is copyright {new Date().getFullYear()}. Boiled down to smaller chunks, it can be described with the following conditions. ## It requires you to: - Keep the license and copyright notice included in Bootstrap’s CSS and JavaScript files when you use them in your works ## It permits you to: - Freely download and use Bootstrap, in whole or in part, for personal, private, company internal, or commercial purposes - Use Bootstrap in packages or distributions that you create - Modify the source code - Grant a sublicense to modify and distribute Bootstrap to third parties not included in the license ## It forbids you to: - Hold the authors and license owners liable for damages as Bootstrap is provided without warranty - Hold the creators or copyright holders of Bootstrap liable - Redistribute any piece of Bootstrap without proper attribution - Use any marks owned by Bootstrap in any way that might state or imply that Bootstrap endorses your distribution - Use any marks owned by Bootstrap in any way that might state or imply that you created the Bootstrap software in question ## It does not require you to: - Include the source of Bootstrap itself, or of any modifications you may have made to it, in any redistribution you may assemble that includes it - Submit changes that you make to Bootstrap back to the Bootstrap project (though such feedback is encouraged) The full Bootstrap license is located [in the project repository]([[config:repo]]/blob/v[[config:current_version]]/LICENSE) for more information. ================================================ FILE: site/src/content/docs/about/overview.mdx ================================================ --- title: About Bootstrap description: Learn more about the team maintaining Bootstrap, how and why the project started, and how to get involved. aliases: - "/about/" - "/docs/[[config:docs_version]]/about/" --- ## Team Bootstrap is maintained by a [small team of developers](https://github.com/orgs/twbs/people) on GitHub. We’re actively looking to grow this team and would love to hear from you if you’re excited about CSS at scale, writing and maintaining vanilla JavaScript plugins, and improving build tooling processes for frontend code. ## History Originally created by a designer and a developer at Twitter, Bootstrap has become one of the most popular front-end frameworks and open source projects in the world. Bootstrap was created at Twitter in mid-2010 by [@mdo](https://x.com/mdo) and [@fat](https://x.com/fat). Prior to being an open-sourced framework, Bootstrap was known as _Twitter Blueprint_. A few months into development, Twitter held its [first Hack Week](https://blog.x.com/engineering/en_us/a/2010/hack-week) and the project exploded as developers of all skill levels jumped in without any external guidance. It served as the style guide for internal tools development at the company for over a year before its public release, and continues to do so today. Originally [released](https://blog.x.com/developer/en_us/a/2011/bootstrap-twitter) on , we’ve since had over [twenty releases]([[config:repo]]/releases), including two major rewrites with v2 and v3. With Bootstrap 2, we added responsive functionality to the entire framework as an optional stylesheet. Building on that with Bootstrap 3, we rewrote the library once more to make it responsive by default with a mobile first approach. With Bootstrap 4, we once again rewrote the project to account for two key architectural changes: a migration to Sass and the move to CSS’s flexbox. Our intention is to help in a small way to move the web development community forward by pushing for newer CSS properties, fewer dependencies, and new technologies across more modern browsers. Our latest release, Bootstrap 5, focuses on improving v4’s codebase with as few major breaking changes as possible. We improved existing features and components, removed support for older browsers, dropped jQuery for regular JavaScript, and embraced more future-friendly technologies like CSS custom properties as part of our tooling. ## Get involved Get involved with Bootstrap development by [opening an issue]([[config:repo]]/issues/new/choose) or submitting a pull request. Read our [contributing guidelines]([[config:repo]]/blob/v[[config:current_version]]/.github/CONTRIBUTING.md) for information on how we develop. ================================================ FILE: site/src/content/docs/about/team.mdx ================================================ --- title: Team description: An overview of the founding team and core contributors to Bootstrap. --- import { getData } from '@libs/data' Bootstrap is maintained by the founding team and a small group of invaluable core contributors, with the massive support and involvement of our community.
    {getData('core-team').map((member) => { return ( {`@${member.user}`} {member.name} @{member.user} ) })}
    Get involved with Bootstrap development by [opening an issue]([[config:repo]]/issues/new/choose) or submitting a pull request. Read our [contributing guidelines]([[config:repo]]/blob/v[[config:current_version]]/.github/CONTRIBUTING.md) for information on how we develop. ================================================ FILE: site/src/content/docs/about/translations.mdx ================================================ --- title: Translations description: Links to community-translated Bootstrap documentation sites. --- import { getData } from '@libs/data' Community members have translated Bootstrap’s documentation into various languages. None are officially supported and they may not always be up-to-date. **We don’t help organize or host translations, we just link to them.** Finished a new or better translation? Open a pull request to add it to our list. ================================================ FILE: site/src/content/docs/components/accordion.mdx ================================================ --- title: Accordion description: Build vertically collapsing accordions in combination with our Collapse JavaScript plugin. aliases: - "/components/" - "/docs/[[config:docs_version]]/components/" toc: true --- ## How it works The accordion uses [collapse]([[docsref:/components/collapse]]) internally to make it collapsible. ## Example Click the accordions below to expand/collapse the accordion content. To render an accordion that’s expanded by default: - add the `.show` class on the `.accordion-collapse` element. - drop the `.collapsed` class from the `.accordion-button` element and set its `aria-expanded` attribute to `true`.

    This is the first item’s accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the second item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the third item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.
    `} /> ### Flush Add `.accordion-flush` to remove some borders and rounded corners to render accordions edge-to-edge with their parent container.

    Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the first item’s accordion body.

    Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the second item’s accordion body. Let’s imagine this being filled with some actual content.

    Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the third item’s accordion body. Nothing more exciting happening here in terms of content, but just filling up the space to make it look, at least at first glance, a bit more representative of how this would look in a real-world application.
    `} /> ### Always open Omit the `data-bs-parent` attribute on each `.accordion-collapse` to make accordion items stay open when another item is opened.

    This is the first item’s accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the second item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.

    This is the third item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.
    `} /> ## Accessibility Please read the [collapse accessibility section]([[docsref:/components/collapse#accessibility]]) for more information. ## CSS ### Variables As part of Bootstrap’s evolving CSS variables approach, accordions now use local CSS variables on `.accordion` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. ### Sass variables ## Usage The collapse plugin utilizes a few classes to handle the heavy lifting: - `.collapse` hides the content - `.collapse.show` shows the content - `.collapsing` is added when the transition starts, and removed when it finishes These classes can be found in `_transitions.scss`. ### Via data attributes Just add `data-bs-toggle="collapse"` and a `data-bs-target` to the element to automatically assign control of one or more collapsible elements. The `data-bs-target` attribute accepts a CSS selector to apply the collapse to. Be sure to add the class `collapse` to the collapsible element. If you’d like it to default open, add the additional class `show`. To add accordion group management to a collapsible area, add the data attribute `data-bs-parent="#selector"`. ### Via JavaScript Enable manually with: ```js const accordionCollapseElementList = document.querySelectorAll('#myAccordion .collapse') const accordionCollapseList = [...accordionCollapseElementList].map(accordionCollapseEl => new bootstrap.Collapse(accordionCollapseEl)) ``` ### Options | Name | Type | Default | Description | | --- | --- | --- | --- | `parent` | selector, DOM element | `null` | If parent is provided, then all collapsible elements under the specified parent will be closed when this collapsible item is shown. (similar to traditional accordion behavior - this is dependent on the `card` class). The attribute has to be set on the target collapsible area. | `toggle` | boolean | `true` | Toggles the collapsible element on invocation. | ### Methods Activates your content as a collapsible element. Accepts an optional options `object`. You can create a collapse instance with the constructor, for example: ```js const bsCollapse = new bootstrap.Collapse('#myCollapse', { toggle: false }) ``` | Method | Description | | --- | --- | | `dispose` | Destroys an element’s collapse. (Removes stored data on the DOM element) | | `getInstance` | Static method which allows you to get the collapse instance associated to a DOM element, you can use it like this: `bootstrap.Collapse.getInstance(element)`. | | `getOrCreateInstance` | Static method which returns a collapse instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Collapse.getOrCreateInstance(element)`. | | `hide` | Hides a collapsible element. **Returns to the caller before the collapsible element has actually been hidden** (e.g., before the `hidden.bs.collapse` event occurs). | | `show` | Shows a collapsible element. **Returns to the caller before the collapsible element has actually been shown** (e.g., before the `shown.bs.collapse` event occurs). | | `toggle` | Toggles a collapsible element to shown or hidden. **Returns to the caller before the collapsible element has actually been shown or hidden** (i.e. before the `shown.bs.collapse` or `hidden.bs.collapse` event occurs). | ### Events Bootstrap’s collapse class exposes a few events for hooking into collapse functionality. | Event type | Description | | --- | --- | | `hide.bs.collapse` | This event is fired immediately when the `hide` method has been called. | | `hidden.bs.collapse` | This event is fired when a collapse element has been hidden from the user (will wait for CSS transitions to complete). | | `show.bs.collapse` | This event fires immediately when the `show` instance method is called. | | `shown.bs.collapse` | This event is fired when a collapse element has been made visible to the user (will wait for CSS transitions to complete). | ```js const myCollapsible = document.getElementById('myCollapsible') myCollapsible.addEventListener('hidden.bs.collapse', event => { // do something... }) ``` ================================================ FILE: site/src/content/docs/components/alerts.mdx ================================================ --- title: Alerts description: Provide contextual feedback messages for typical user actions with the handful of available and flexible alert messages. toc: true --- import { getData } from '@libs/data' ## Examples Alerts are available for any length of text, as well as an optional close button. For proper styling, use one of the eight **required** contextual classes (e.g., `.alert-success`). For inline dismissal, use the [alerts JavaScript plugin](#dismissing). **Heads up!** As of v5.3.0, the `alert-variant()` Sass mixin is deprecated. Alert variants now have their CSS variables overridden in [a Sass loop](#sass-loops). ``)} /> ### Live example Click the button below to show an alert (hidden with inline styles to start), then dismiss (and destroy) it with the built-in close button. `} /> We use the following JavaScript to trigger our live alert demo: ### Link color Use the `.alert-link` utility class to quickly provide matching colored links within any alert. ``)} /> ### Additional content Alerts can also contain additional HTML elements like headings, paragraphs and dividers.

    Well done!

    Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.


    Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

    `} /> ### Icons Similarly, you can use [flexbox utilities]([[docsref:/utilities/flex]]) and [Bootstrap Icons]([[config:icons]]) to create alerts with icons. Depending on your icons and content, you may want to add more utilities or custom styles.
    An example alert with an icon
    `} /> Need more than one icon for your alerts? Consider using more Bootstrap Icons and making a local SVG sprite like so to easily reference the same icons repeatedly. `} /> ### Dismissing Using the alert JavaScript plugin, it’s possible to dismiss any alert inline. Here’s how: - Be sure you’ve loaded the alert plugin, or the compiled Bootstrap JavaScript. - Add a [close button]([[docsref:/components/close-button]]) and the `.alert-dismissible` class, which adds extra padding to the right of the alert and positions the close button. - On the close button, add the `data-bs-dismiss="alert"` attribute, which triggers the JavaScript functionality. Be sure to use the ` `} /> When an alert is dismissed, the element is completely removed from the page structure. If a keyboard user dismisses the alert using the close button, their focus will suddenly be lost and, depending on the browser, reset to the start of the page/document. For this reason, we recommend including additional JavaScript that listens for the `closed.bs.alert` event and programmatically sets `focus()` to the most appropriate location in the page. If you’re planning to move focus to a non-interactive element that normally does not receive focus, make sure to add `tabindex="-1"` to the element. ## CSS ### Variables As part of Bootstrap’s evolving CSS variables approach, alerts now use local CSS variables on `.alert` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. ### Sass variables ### Sass mixins ### Sass loops Loop that generates the modifier classes with an overriding of CSS variables. ## JavaScript behavior ### Initialize Initialize elements as alerts ```js const alertList = document.querySelectorAll('.alert') const alerts = [...alertList].map(element => new bootstrap.Alert(element)) ``` For the sole purpose of dismissing an alert, it isn’t necessary to initialize the component manually via the JS API. By making use of `data-bs-dismiss="alert"`, the component will be initialized automatically and properly dismissed. See the [triggers](#triggers) section for more details. ### Triggers **Note that closing an alert will remove it from the DOM.** ### Methods You can create an alert instance with the alert constructor, for example: ```js const bsAlert = new bootstrap.Alert('#myAlert') ``` This makes an alert listen for click events on descendant elements which have the `data-bs-dismiss="alert"` attribute. (Not necessary when using the data-api’s auto-initialization.) | Method | Description | | --- | --- | | `close` | Closes an alert by removing it from the DOM. If the `.fade` and `.show` classes are present on the element, the alert will fade out before it is removed. | | `dispose` | Destroys an element’s alert. (Removes stored data on the DOM element) | | `getInstance` | Static method which allows you to get the alert instance associated to a DOM element. For example: `bootstrap.Alert.getInstance(alert)`. | | `getOrCreateInstance` | Static method which returns an alert instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Alert.getOrCreateInstance(element)`. | Basic usage: ```js const alert = bootstrap.Alert.getOrCreateInstance('#myAlert') alert.close() ``` ### Events Bootstrap’s alert plugin exposes a few events for hooking into alert functionality. | Event | Description | | --- | --- | | `close.bs.alert` | Fires immediately when the `close` instance method is called. | | `closed.bs.alert` | Fired when the alert has been closed and CSS transitions have completed. | ```js const myAlert = document.getElementById('myAlert') myAlert.addEventListener('closed.bs.alert', event => { // do something, for instance, explicitly move focus to the most appropriate element, // so it doesn’t get lost/reset to the start of the page // document.getElementById('...').focus() }) ``` ================================================ FILE: site/src/content/docs/components/badge.mdx ================================================ --- title: Badges description: Documentation and examples for badges, our small count and labeling component. toc: true --- import { getData } from '@libs/data' ## Examples Badges scale to match the size of the immediate parent element by using relative font sizing and `em` units. As of v5, badges no longer have focus or hover styles for links. ### Headings Example heading New

    Example heading New

    Example heading New

    Example heading New

    Example heading New
    Example heading New
    `} /> ### Buttons Badges can be used as part of links or buttons to provide a counter. Notifications 4 `} /> Note that depending on how they are used, badges may be confusing for users of screen readers and similar assistive technologies. While the styling of badges provides a visual cue as to their purpose, these users will simply be presented with the content of the badge. Depending on the specific situation, these badges may seem like random additional words or numbers at the end of a sentence, link, or button. Unless the context is clear (as with the “Notifications” example, where it is understood that the “4” is the number of notifications), consider including additional context with a visually hidden piece of additional text. ### Positioned Use utilities to modify a `.badge` and position it in the corner of a link or button. Inbox 99+ unread messages `} /> You can also replace the `.badge` class with a few more utilities without a count for a more generic indicator. Profile New alerts `} /> ## Background colors Set a `background-color` with contrasting foreground `color` with [our `.text-bg-{color}` helpers]([[docsref:helpers/color-background]]). Previously it was required to manually pair your choice of [`.text-{color}`]([[docsref:/utilities/colors]]) and [`.bg-{color}`]([[docsref:/utilities/background]]) utilities for styling, which you still may use if you prefer. `${themeColor.title}`)} /> ## Pill badges Use the `.rounded-pill` utility class to make badges more rounded with a larger `border-radius`. `${themeColor.title}`)} /> ## CSS ### Variables As part of Bootstrap’s evolving CSS variables approach, badges now use local CSS variables on `.badge` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. ### Sass variables ================================================ FILE: site/src/content/docs/components/breadcrumb.mdx ================================================ --- title: Breadcrumb description: Indicate the current page’s location within a navigational hierarchy that automatically adds separators via CSS. toc: true --- ## Example Use an ordered or unordered list with linked list items to create a minimally styled breadcrumb. Use our utilities to add additional styles as desired. `} /> ## Dividers Dividers are automatically added in CSS through [`::before`](https://developer.mozilla.org/en-US/docs/Web/CSS/::before) and [`content`](https://developer.mozilla.org/en-US/docs/Web/CSS/content). They can be changed by modifying a local CSS custom property `--bs-breadcrumb-divider`, or through the `$breadcrumb-divider` Sass variable — and `$breadcrumb-divider-flipped` for its RTL counterpart, if needed. We default to our Sass variable, which is set as a fallback to the custom property. This way, you get a global divider that you can override without recompiling CSS at any time. `} /> When modifying via Sass, the [quote](https://sass-lang.com/documentation/modules/string/#quote) function is required to generate the quotes around a string. For example, using `>` as the divider, you can use this: ```scss $breadcrumb-divider: quote(">"); ``` It’s also possible to use an **embedded SVG icon**. Apply it via our CSS custom property, or use the Sass variable. **Inlined SVG requires properly escaped characters.** Some reserved characters, such as `<`, `>` and `#`, must be URL-encoded or escaped. We do this with the `$breadcrumb-divider` variable using our [`escape-svg()` Sass function]([[docsref:/customize/sass#escape-svg]]). When customizing the CSS variable, you must handle this yourself. Read [Kevin Weber’s explanations on CodePen](https://codepen.io/kevinweber/pen/dXWoRw ) for more info. `} /> ```scss $breadcrumb-divider: url("data:image/svg+xml,"); ``` You can also remove the divider setting `--bs-breadcrumb-divider: '';` (empty strings in CSS custom properties counts as a value), or setting the Sass variable to `$breadcrumb-divider: none;`. `} /> ```scss $breadcrumb-divider: none; ``` ## Accessibility Since breadcrumbs provide a navigation, it’s a good idea to add a meaningful label such as `aria-label="breadcrumb"` to describe the type of navigation provided in the ``} /> ## Directions **Directions are flipped in RTL mode.** As such, `.dropstart` will appear on the right side. ### Centered Make the dropdown menu centered below the toggle with `.dropdown-center` on the parent element. `} /> ### Dropup Trigger dropdown menus above elements by adding `.dropup` to the parent element. `} /> ```html
    ``` ### Dropup centered Make the dropup menu centered above the toggle with `.dropup-center` on the parent element. `} /> ### Dropend Trigger dropdown menus at the right of the elements by adding `.dropend` to the parent element. `} /> ```html
    ``` ### Dropstart Trigger dropdown menus at the left of the elements by adding `.dropstart` to the parent element.
    `} /> ```html
    ``` ## Menu items You can use `` or ` `} /> You can also create non-interactive dropdown items with `.dropdown-item-text`. Feel free to style further with custom CSS or text utilities.
  • 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... }) ``` ================================================ FILE: site/src/content/docs/components/list-group.mdx ================================================ --- 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. A list item
        4. A list item
        `} /> These work great with custom content as well.
      1. Subheading
        Content for list item
        14
      2. Subheading
        Content for list item
        14
      3. Subheading
        Content for list item
        14
      `} /> ## 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 ` ```html ``` In the above static example, we use `
      `, to avoid issues with the heading hierarchy in the documentation page. Structurally, however, a modal dialog represents its own separate document/context, so the `.modal-title` should ideally be an `

      `. If necessary, you can use the [font size utilities]([[docsref:/utilities/text#font-size]]) to control the heading’s appearance. All the following live examples use this approach. ### Live demo Toggle a working modal demo by clicking the button below. It will slide down and fade in from the top of the page. Launch demo modal `} /> ```html ``` ### Static backdrop When backdrop is set to static, the modal will not close when clicking outside of it. Click the button below to try it. Launch static backdrop modal `} /> ```html ``` ### Scrolling long content When modals become too long for the user’s viewport or device, they scroll independent of the page itself. Try the demo below to see what we mean. Launch demo modal `} /> You can also create a scrollable modal that allows scrolling the modal body by adding `.modal-dialog-scrollable` to `.modal-dialog`. Launch demo modal `} /> ```html ``` ### Vertically centered Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal.

      `} /> ## 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`. Display headings are customizable via two variables, `$display-font-family` and `$display-font-style`. ## Lead Make a paragraph stand out by adding `.lead`. This is a lead paragraph. It stands out from regular paragraphs.

      `} /> ## Inline text elements Styling for common inline HTML5 elements. 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.

      `} /> 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. attr

      HTML

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

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

      `} /> ### 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.

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

      `} /> ### Alignment Use text utilities as needed to change the alignment of your blockquote.

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

      `} />

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

      `} /> ## 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.
    5. This is a list.
    6. It appears completely unstyled.
    7. Structurally, it’s still a list.
    8. However, this style only applies to immediate child elements.
    9. Nested lists:
      • are unaffected by this style
      • will still show a bullet
      • and have appropriate left margin
    10. This may still come in handy in some situations.
    11. `} /> ### Inline Remove a list’s bullets and apply some light `margin` with a combination of two classes, `.list-inline` and `.list-inline-item`.
    12. This is a list item.
    13. And another one.
    14. But they’re displayed inline.
    15. `} /> ### 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.
      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.
      `} /> ## 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. ## CSS ### Sass variables Headings have some dedicated variables for sizing and spacing. Miscellaneous typography elements covered here and in [Reboot]([[docsref:/content/reboot]]) also have dedicated variables. ### Sass mixins There are no dedicated mixins for typography, but Bootstrap does use [Responsive Font Sizing (RFS)]([[docsref:/getting-started/rfs]]). ================================================ FILE: site/src/content/docs/customize/color-modes.mdx ================================================ --- title: Color modes description: Bootstrap now supports color modes, or themes, as of v5.3.0. Explore our default light color mode and the new dark mode, or create your own using our styles as your template. toc: true added: version: "5.3" --- import { getDocsRelativePath } from '@libs/path' **Try it yourself!** Download the source code and working demo for using Bootstrap with Stylelint, and the color modes from the [twbs/examples repository](https://github.com/twbs/examples/tree/main/color-modes). You can also [open the example in StackBlitz](https://stackblitz.com/github/twbs/examples/tree/main/color-modes?file=index.html). ## Dark mode **Bootstrap now supports color modes, starting with dark mode!** With v5.3.0 you can implement your own color mode toggler (see below for an example from Bootstrap’s docs) and apply the different color modes as you see fit. We support a light mode (default) and now dark mode. Color modes can be toggled globally on the `` element, or on specific components and elements, thanks to the `data-bs-theme` attribute. Alternatively, you can also switch to a media query implementation thanks to our color mode mixin—see [the usage section for details](#building-with-sass). Heads up though—this eliminates your ability to change themes on a per-component basis as shown below. ## Example For example, to change the color mode of a dropdown menu, add `data-bs-theme="light"` or `data-bs-theme="dark"` to the parent `.dropdown`. Now, no matter the global color mode, these dropdowns will display with the specified theme value. `} /> ## How it works - As shown above, color mode styles are controlled by the `data-bs-theme` attribute. This attribute can be applied to the `` element, or to any other element or Bootstrap component. If applied to the `` element, it will apply to everything. If applied to a component or element, it will be scoped to that specific component or element. - For each color mode you wish to support, you’ll need to add new overrides for the shared global CSS variables. We do this already in our `_root.scss` stylesheet for dark mode, with light mode being the default values. In writing color mode specific styles, use the mixin: ```scss // Color mode variables in _root.scss @include color-mode(dark) { // CSS variable overrides here... } ``` - We use a custom `_variables-dark.scss` to power those shared global CSS variable overrides for dark mode. This file isn’t required for your own custom color modes, but it’s required for our dark mode for two reasons. First, it’s better to have a single place to reset global colors. Second, some Sass variables had to be overridden for background images embedded in our CSS for accordions, form components, and more. ## Usage ### Enable dark mode Enable the built in dark color mode across your entire project by adding the `data-bs-theme="dark"` attribute to the `` element. This will apply the dark color mode to all components and elements, other than those with a specific `data-bs-theme` attribute applied. Building on the [quick start template]([[docsref:/getting-started/introduction#quick-start]]): ```html Bootstrap demo

      Hello, world!

      ``` Bootstrap does not yet ship with a built-in color mode picker, but you can use the one from our own documentation if you like. [Learn more in the JavaScript section.](#javascript) ### Building with Sass Our new dark mode option is available to use for all users of Bootstrap, but it’s controlled via data attributes instead of media queries and does not automatically toggle your project’s color mode. You can disable our dark mode entirely via Sass by changing `$enable-dark-mode` to `false`. We use a custom Sass mixin, `color-mode()`, to help you control _how_ color modes are applied. By default, we use a `data` attribute approach, allowing you to create more user-friendly experiences where your visitors can choose to have an automatic dark mode or control their preference (like in our own docs here). This is also an easy and scalable way to add different themes and more custom color modes beyond light and dark. In case you want to use media queries and only make color modes automatic, you can change the mixin’s default type via Sass variable. Consider the following snippet and its compiled CSS output. ```scss $color-mode-type: data; @include color-mode(dark) { .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } } ``` Outputs to: ```css [data-bs-theme=dark] .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } ``` And when setting to `media-query`: ```scss $color-mode-type: media-query; @include color-mode(dark) { .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } } ``` Outputs to: ```css @media (prefers-color-scheme: dark) { .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } } ``` ## Custom color modes While the primary use case for color modes is light and dark mode, custom color modes are also possible. Create your own `data-bs-theme` selector with a custom value as the name of your color mode, then modify our Sass and CSS variables as needed. We opted to create a separate `_variables-dark.scss` stylesheet to house Bootstrap’s dark mode specific Sass variables, but that’s not required for you. For example, you can create a “blue theme” with the selector `data-bs-theme="blue"`. In your custom Sass or CSS file, add the new selector and override any global or component CSS variables as needed. If you’re using Sass, you can also use Sass’s functions within your CSS variable overrides.
      Example blue theme

      Some paragraph text to show how the blue theme might look with written copy.


      `} /> ```html
      ...
      ``` ## JavaScript To allow visitors or users to toggle color modes, you’ll need to create a toggle element to control the `data-bs-theme` attribute on the root element, ``. We’ve built a toggler in our documentation that initially defers to a user’s current system color mode, but provides an option to override that and pick a specific color mode. Here’s a look at the JavaScript that powers it. Feel free to inspect our own documentation navbar to see how it’s implemented using HTML and CSS from our own components. It is suggested to include the JavaScript at the top of your page to reduce potential screen flickering during reloading of your site. Note that if you decide to use media queries for your color modes, your JavaScript may need to be modified or removed if you prefer an implicit control. ## Adding theme colors Adding a new color in `$theme-colors` is not enough for some of our components like [alerts]([[docsref:/components/alerts]]) and [list groups]([[docsref:/components/list-group]]). New colors must also be defined in `$theme-colors-text`, `$theme-colors-bg-subtle`, and `$theme-colors-border-subtle` for light theme; but also in `$theme-colors-text-dark`, `$theme-colors-bg-subtle-dark`, and `$theme-colors-border-subtle-dark` for dark theme. This is a manual process because Sass cannot generate its own Sass variables from an existing variable or map. In future versions of Bootstrap, we'll revisit this setup to reduce the duplication. ```scss // Required @import "functions"; @import "variables"; @import "variables-dark"; // Add a custom color to $theme-colors $custom-colors: ( "custom-color": #712cf9 ); $theme-colors: map-merge($theme-colors, $custom-colors); @import "maps"; @import "mixins"; @import "utilities"; // Add a custom color to new theme maps // Light mode $custom-colors-text: ("custom-color": #712cf9); $custom-colors-bg-subtle: ("custom-color": #e1d2fe); $custom-colors-border-subtle: ("custom-color": #bfa1fc); $theme-colors-text: map-merge($theme-colors-text, $custom-colors-text); $theme-colors-bg-subtle: map-merge($theme-colors-bg-subtle, $custom-colors-bg-subtle); $theme-colors-border-subtle: map-merge($theme-colors-border-subtle, $custom-colors-border-subtle); // Dark mode $custom-colors-text-dark: ("custom-color": #e1d2f2); $custom-colors-bg-subtle-dark: ("custom-color": #8951fa); $custom-colors-border-subtle-dark: ("custom-color": #e1d2f2); $theme-colors-text-dark: map-merge($theme-colors-text-dark, $custom-colors-text-dark); $theme-colors-bg-subtle-dark: map-merge($theme-colors-bg-subtle-dark, $custom-colors-bg-subtle-dark); $theme-colors-border-subtle-dark: map-merge($theme-colors-border-subtle-dark, $custom-colors-border-subtle-dark); // Remainder of Bootstrap imports @import "root"; @import "reboot"; // etc ``` ## CSS ### Variables Dozens of root level CSS variables are repeated as overrides for dark mode. These are scoped to the color mode selector, which defaults to `data-bs-theme` but [can be configured](#building-with-sass) to use a `prefers-color-scheme` media query. Use these variables as a guideline for generating your own new color modes. ### Sass variables CSS variables for our dark color mode are partially generated from dark mode specific Sass variables in `_variables-dark.scss`. This also includes some custom overrides for changing the colors of embedded SVGs used throughout our components. ### Sass mixins Styles for dark mode, and any custom color modes you create, can be scoped appropriately to the `data-bs-theme` attribute selector or media query with the customizable `color-mode()` mixin. See the [Sass usage section](#building-with-sass) for more details. ================================================ FILE: site/src/content/docs/customize/color.mdx ================================================ --- 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. toc: true --- import { getData } from '@libs/data' import { getSequence } from '@libs/utils' ## Colors Bootstrap’s color palette has continued to expand and become more nuanced in v5.3.0. We’ve added new variables for `secondary` and `tertiary` text and background colors, plus `{color}-bg-subtle`, `{color}-border-subtle`, and `{color}-text-emphasis` for our theme colors. These new colors are available through Sass and CSS variables (but not our color maps or utility classes) with the express goal of making it easier to customize across multiple colors modes like light and dark. These new variables are globally set on `:root` and are adapted for our new dark color mode while our original theme colors remain unchanged. Colors ending in `-rgb` provide the `red, green, blue` values for use in `rgb()` and `rgba()` color modes. For example, `rgba(var(--bs-secondary-bg-rgb), .5)`. **Heads up!** There’s some potential confusion with our new secondary and tertiary colors, and our existing secondary theme color, as well as our light and dark theme colors. Expect this to be ironed out in v6.
      Description Swatch Variables
      **Body —** Default foreground (color) and background, including components.
       
      `--bs-body-color`
      `--bs-body-color-rgb`
       
      `--bs-body-bg`
      `--bs-body-bg-rgb`
      **Secondary —** Use the `color` option for lighter text. Use the `bg` option for dividers and to indicate disabled component states.
       
      `--bs-secondary-color`
      `--bs-secondary-color-rgb`
       
      `--bs-secondary-bg`
      `--bs-secondary-bg-rgb`
      **Tertiary —** Use the `color` option for even lighter text. Use the `bg` option to style backgrounds for hover states, accents, and wells.
       
      `--bs-tertiary-color`
      `--bs-tertiary-color-rgb`
       
      `--bs-tertiary-bg`
      `--bs-tertiary-bg-rgb`
      **Emphasis —** For higher contrast text. Not applicable for backgrounds.
       
      `--bs-emphasis-color`
      `--bs-emphasis-color-rgb`
      **Border —** For component borders, dividers, and rules. Use `--bs-border-color-translucent` to blend with backgrounds with an `rgba()` value.
       
      `--bs-border-color`
      `--bs-border-color-rgb`
      **Primary —** Main theme color, used for hyperlinks, focus styles, and component and form active states.
       
      `--bs-primary`
      `--bs-primary-rgb`
       
      `--bs-primary-bg-subtle`
       
      `--bs-primary-border-subtle`
      Text
      `--bs-primary-text-emphasis`
      **Success —** Theme color used for positive or successful actions and information.
       
      `--bs-success`
      `--bs-success-rgb`
       
      `--bs-success-bg-subtle`
       
      `--bs-success-border-subtle`
      Text
      `--bs-success-text-emphasis`
      **Danger —** Theme color used for errors and dangerous actions.
       
      `--bs-danger`
      `--bs-danger-rgb`
       
      `--bs-danger-bg-subtle`
       
      `--bs-danger-border-subtle`
      Text
      `--bs-danger-text-emphasis`
      **Warning —** Theme color used for non-destructive warning messages.
       
      `--bs-warning`
      `--bs-warning-rgb`
       
      `--bs-warning-bg-subtle`
       
      `--bs-warning-border-subtle`
      Text
      `--bs-warning-text-emphasis`
      **Info —** Theme color used for neutral and informative content.
       
      `--bs-info`
      `--bs-info-rgb`
       
      `--bs-info-bg-subtle`
       
      `--bs-info-border-subtle`
      Text
      `--bs-info-text-emphasis`
      **Light —** Additional theme option for less contrasting colors.
       
      `--bs-light`
      `--bs-light-rgb`
       
      `--bs-light-bg-subtle`
       
      `--bs-light-border-subtle`
      Text
      `--bs-light-text-emphasis`
      **Dark —** Additional theme option for higher contrasting colors.
       
      `--bs-dark`
      `--bs-dark-rgb`
       
      `--bs-dark-bg-subtle`
       
      `--bs-dark-border-subtle`
      Text
      `--bs-dark-text-emphasis`
      ### Using the new colors These new colors are accessible via CSS variables and utility classes—like `--bs-primary-bg-subtle` and `.bg-primary-subtle`—allowing you to compose your own CSS rules with the variables, or to quickly apply styles via classes. The utilities are built with the color’s associated CSS variables, and since we customize those CSS variables for dark mode, they are also adaptive to color mode by default. Example element with utilities `} /> ### 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.
      {getData('theme-colors').map((themeColor) => { return (
      {themeColor.title}
      ) })}
      All these colors are available as a Sass map, `$theme-colors`. 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.
      {getData('colors').map((color) => { if ((color.name !== "white") && (color.name !== "gray") && (color.name !== "gray-dark")) { return (
      ${color.name} {color.hex}
      {getSequence(100, 900, 100).map((value) => { return (
      ${color.name}-{value}
      ) })}
      ) } })}
      $gray-500#adb5bd
      {getData('grays').map((gray) => { return (
      $gray-{gray.name}
      ) })}
      $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: 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 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/variables-dark"; @import "bootstrap/scss/maps"; @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. ================================================ FILE: site/src/content/docs/customize/components.mdx ================================================ --- title: Components description: Learn how and why we build nearly all our components responsively and with base and modifier classes. 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. ## 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. Should you modify your `$grid-breakpoints`, your changes will apply to all the loops iterating over that map. For more information and examples on how to modify our Sass maps and variables, please refer to [the CSS section of the Grid documentation]([[docsref:/layout/grid#css]]). ## 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: **This is an info callout.** Example text to show it in action. **This is a warning callout.** Example text to show it in action. **This is a danger callout.** Example text to show it in action. ================================================ FILE: site/src/content/docs/customize/css-variables.mdx ================================================ --- title: CSS variables description: Use Bootstrap’s CSS custom properties for fast and forward-looking design and development. 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. ### Default These CSS variables are available everywhere, regardless of color mode. ### Dark mode These variables are scoped to our built-in dark mode. ## Component variables Bootstrap 5 is increasingly making use of custom properties as local variables for various components. This way we 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]]). Our [navbars also use CSS variables]([[docsref:/components/navbar#css]]) as of v5.2.0. We’re also using CSS variables across our grids—primarily for gutters the [new opt-in CSS grid]([[docsref:/layout/css-grid]])—with more component usage coming in the future. Whenever possible, we'll assign CSS variables at the base component level (e.g., `.navbar` for navbar and its sub-components). This reduces guessing on where and how to customize, and allows for easy modifications by our team in future updates. ## Prefix Most CSS variables use a prefix to avoid collisions with your own codebase. This prefix is in addition to the `--` that’s required on every CSS variable. Customize the prefix via the `$prefix` Sass variable. By default, it’s set to `bs-` (note the trailing dash). ## 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); } ``` ## Focus variables Bootstrap provides custom `:focus` styles using a combination of Sass and CSS variables that can be optionally added to specific components and elements. We do not yet globally override all `:focus` styles. In our Sass, we set default values that can be customized before compiling. Those variables are then reassigned to `:root` level CSS variables that can be customized in real-time, including with options for `x` and `y` offsets (which default to their fallback value of `0`). ## 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 meantime, you can use these variables in other CSS situations, as well as in your JavaScript. ================================================ FILE: site/src/content/docs/customize/optimize.mdx ================================================ --- title: Optimize description: Keep your projects lean, responsive, and maintainable so you can deliver the best experience and focus on more important jobs. 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`. 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, Parcel, or Vite, 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. **Heads up!** Files in `bootstrap/js/dist` use the **default export**. To use them, do the following: ```js import Modal from 'bootstrap/js/dist/modal' const modal = new Modal(document.getElementById('myModal')) ``` ## 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: - https://medium.com/dwarves-foundation/remove-unused-css-styles-from-bootstrap-using-purgecss-88395a2c5772 - https://lukelowrey.com/automatically-removeunused-css-from-bootstrap-or-other-frameworks/ 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. ## Non-blocking files While minifying and using compression might seem like enough, making your files non-blocking ones is also a big step in making your site well-optimized and fast enough. If you are using a [Lighthouse](https://developer.chrome.com/docs/lighthouse/overview/) plugin in Google Chrome, you may have stumbled over FCP. [The First Contentful Paint](https://web.dev/articles/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: - https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources/ - https://web.dev/articles/defer-non-critical-css ## 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. ================================================ FILE: site/src/content/docs/customize/options.mdx ================================================ --- title: Options description: Quickly customize Bootstrap with built-in variables to easily toggle global CSS preferences for controlling style and behavior. --- Customize Bootstrap with our built-in custom variables file and easily toggle global CSS preferences with new `$enable-*` Sass variables. Override a variable’s value and recompile with `npm run test` as needed. You can find and customize these variables for key global options in Bootstrap’s `scss/_variables.scss` file. | Variable | Values | Description | | ------------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------- | | `$spacer` | `1rem` (default), or any value > 0 | Specifies the default spacer value to programmatically generate our [spacer utilities]([[docsref:/utilities/spacing]]). | | `$enable-dark-mode` | `true` (default) or `false` | Enables built-in [dark mode support]([[docsref:/customize/color-modes#dark-mode]]) across the project and its components. | | `$enable-rounded` | `true` (default) or `false` | Enables predefined `border-radius` styles on various components. | | `$enable-shadows` | `true` or `false` (default) | Enables predefined decorative `box-shadow` styles on various components. Does not affect `box-shadow`s used for focus states. | | `$enable-gradients` | `true` or `false` (default) | Enables predefined gradients via `background-image` styles on various components. | | `$enable-transitions` | `true` (default) or `false` | Enables predefined `transition`s on various components. | | `$enable-reduced-motion` | `true` (default) or `false` | Enables the [`prefers-reduced-motion` media query]([[docsref:/getting-started/accessibility#reduced-motion]]), which suppresses certain animations/transitions based on the users’ browser/operating system preferences. | | `$enable-grid-classes` | `true` (default) or `false` | Enables the generation of CSS classes for the grid system (e.g. `.row`, `.col-md-1`, etc.). | | `$enable-cssgrid` | `true` or `false` (default) | Enables the experimental CSS Grid system (e.g. `.grid`, `.g-col-md-1`, etc.). | | `$enable-container-classes` | `true` (default) or `false` | Enables the generation of CSS classes for layout containers. (New in v5.2.0) | | `$enable-caret` | `true` (default) or `false` | Enables pseudo element caret on `.dropdown-toggle`. | | `$enable-button-pointers` | `true` (default) or `false` | Add “hand” cursor to non-disabled button elements. | | `$enable-rfs` | `true` (default) or `false` | Globally enables [RFS]([[docsref:/getting-started/rfs]]). | | `$enable-validation-icons` | `true` (default) or `false` | Enables `background-image` icons within textual inputs and some custom forms for validation states. | | `$enable-negative-margins` | `true` or `false` (default) | Enables the generation of [negative margin utilities]([[docsref:/utilities/spacing#negative-margin]]). | | `$enable-deprecation-messages` | `true` (default) or `false` | Set to `false` to hide warnings when using any of the deprecated mixins and functions that are planned to be removed in `v6`. | | `$enable-important-utilities` | `true` (default) or `false` | Enables the `!important` suffix in utility classes. | | `$enable-smooth-scroll` | `true` (default) or `false` | Applies `scroll-behavior: smooth` globally, except for users asking for reduced motion through [`prefers-reduced-motion` media query]([[docsref:/getting-started/accessibility#reduced-motion]]) | ================================================ FILE: site/src/content/docs/customize/overview.mdx ================================================ --- title: Customize description: Learn how to theme, customize, and extend Bootstrap with Sass, a boatload of global options, an expansive color system, and more. toc: false aliases: "/docs/[[config:docs_version]]/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: Color modes description: Explore our default light mode and the new dark mode, or create custom color modes yourself. - 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]]) - [Carousel controls]([[docsref:/components/carousel#with-controls]]) - [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]]) - [Navbar toggle buttons]([[docsref:/components/navbar#responsive-behaviors]]) - [Select menus]([[docsref:/forms/select]]) 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]([[docsref:/getting-started/webpack#extracting-svg-files]]), 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. ================================================ FILE: site/src/content/docs/customize/sass.mdx ================================================ --- 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. toc: true --- Utilize our source Sass files to take advantage of variables, maps, mixins, and more. Sass deprecation warnings are shown when compiling source Sass files with the latest versions of Dart Sass. This does not prevent compilation or usage of Bootstrap. We’re [working on a long-term fix]([[config:repo]]/issues/40962), but in the meantime these deprecation notices can be ignored. ## 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/ └── index.html ``` If you’ve downloaded our source files and aren’t using a package manager, you’ll want to manually create something similar to that structure, keeping Bootstrap’s source files separate from your own. ```text your-project/ ├── scss/ │ └── custom.scss ├── bootstrap/ │ ├── js/ │ └── scss/ └── index.html ``` ## 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 (including any separate color mode stylesheets) @import "../node_modules/bootstrap/scss/variables"; @import "../node_modules/bootstrap/scss/variables-dark"; // 4. Include any default map overrides here // 5. Include remainder of required parts @import "../node_modules/bootstrap/scss/maps"; @import "../node_modules/bootstrap/scss/mixins"; @import "../node_modules/bootstrap/scss/root"; // 6. Include any other optional stylesheet partials as desired; list below is not inclusive of all available stylesheets @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"; // ... // 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss` @import "../node_modules/bootstrap/scss/utilities/api"; // 8. 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. ## Compiling In order to use your custom Sass code as CSS in the browser, you need a Sass compiler. Sass ships as a CLI package, but you can also compile it with other build tools like [Gulp](https://gulpjs.com/) or [Webpack](https://webpack.js.org/), or with GUI applications. Some IDEs also have Sass compilers built in or as downloadable extensions. We like to use the CLI to compile our Sass, but you can use whichever method you prefer. From the command line, run the following: ```sh # Install Sass globally npm install -g sass # Watch your custom Sass for changes and compile it to CSS sass --watch ./scss/custom.scss ./css/custom.css ``` Learn more about your options at [sass-lang.com/install](https://sass-lang.com/install/) and [compiling with VS Code](https://code.visualstudio.com/docs/languages/css#_transpiling-sass-and-less-into-css). **Using Bootstrap with another build tool?** Consider reading our guides for compiling with [Webpack]([[docsref:/getting-started/webpack]]), [Parcel]([[docsref:/getting-started/parcel]]), or [Vite]([[docsref:/getting-started/vite]]). We also have production-ready demos in [our examples repository on GitHub](https://github.com/twbs/examples). ## Including Once your CSS is compiled, you can include it in your HTML files. Inside your `index.html` you’ll want to include your compiled CSS file. Be sure to update the path to your compiled CSS file if you’ve changed it. ```html Custom Bootstrap

      Hello, world!

      ``` ## 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/variables-dark"; @import "../node_modules/bootstrap/scss/maps"; @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. ## 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 `$theme-colors` between our requirements just after its definition in `variables` and before its usage in `maps`: ```scss // Required @import "../node_modules/bootstrap/scss/functions"; @import "../node_modules/bootstrap/scss/variables"; @import "../node_modules/bootstrap/scss/variables-dark"; $theme-colors: map-remove($theme-colors, "info", "light", "dark"); @import "../node_modules/bootstrap/scss/maps"; @import "../node_modules/bootstrap/scss/mixins"; @import "../node_modules/bootstrap/scss/root"; // 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. `shift-color()` combines these two functions by shading the color if the weight is positive and tinting the color if the weight is negative. 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%); } .custom-element-3 { color: shift-color($success, 40%); background-color: shift-color($success, -60%); } ``` ### Color contrast In order to meet the [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/TR/WCAG/) contrast requirements, authors **must** provide a minimum [text color contrast of 4.5:1](https://www.w3.org/TR/WCAG/#contrast-minimum) and a minimum [non-text color contrast of 3:1](https://www.w3.org/TR/WCAG/#non-text-contrast), with very few exceptions. To help with this, we included the `color-contrast` function in Bootstrap. It uses the [WCAG contrast ratio algorithm](https://www.w3.org/TR/WCAG/#dfn-contrast-ratio) for calculating contrast thresholds based on [relative luminance](https://www.w3.org/TR/WCAG/#dfn-relative-luminance) in an `sRGB` color space 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` and `dark` color schemes. See [the color modes documentation]([[docsref:/customize/color-modes]]) for information on our color mode mixin. ```scss .custom-element { @include color-scheme(light) { // Insert light mode styles here } @include color-scheme(dark) { // Insert dark mode styles here } } ``` ================================================ FILE: site/src/content/docs/docsref.mdx ================================================ --- title: Docs reference description: Examples of Bootstrap’s documentation-specific components and styles. aliases: "/docsref/" toc: true robots: noindex,follow --- ## Buttons ## Callouts Default callout Warning callout Danger callout ## Code example ```scss .test { --color: blue; } ``` HTML abbreviation element.`} /> This is a test.`} /> ================================================ FILE: site/src/content/docs/extend/approach.mdx ================================================ --- title: Approach description: Learn about the guiding principles, strategies, and techniques used to build and maintain Bootstrap so you can more easily customize and extend it yourself. aliases: - "/docs/[[config:docs_version]]/extend/" --- While the getting started pages provide an introductory tour of the project and what it offers, this document focuses on _why_ we do the things we do in Bootstrap. It explains our philosophy to building on the web so that others can learn from us, contribute with us, and help us improve. See something that doesn’t sound right, or perhaps could be done better? [Open an issue]([[config:repo]]/issues/new/choose)—we’d love to discuss it with you. ## Summary We'll dive into each of these more throughout, but at a high level, here’s what guides our approach. - Components should be responsive and mobile-first - Components should be built with a base class and extended via modifier classes - Component states should obey a common z-index scale - Whenever possible, prefer an HTML and CSS implementation over JavaScript - Whenever possible, use utilities over custom styles - Whenever possible, avoid enforcing strict HTML requirements (children selectors) ## Responsive Bootstrap’s responsive styles are built to be responsive, an approach that’s often referred to as _mobile-first_. We use this term in our docs and largely agree with it, but at times it can be too broad. While not every component _must_ be entirely responsive in Bootstrap, this responsive approach is about reducing CSS overrides by pushing you to add styles as the viewport becomes larger. Across Bootstrap, you’ll see this most clearly in our media queries. In most cases, we use `min-width` queries that begin to apply at a specific breakpoint and carry up through the higher breakpoints. For example, a `.d-none` applies from `min-width: 0` to infinity. On the other hand, a `.d-md-none` applies from the medium breakpoint and up. At times we'll use `max-width` when a component’s inherent complexity requires it. At times, these overrides are functionally and mentally clearer to implement and support than rewriting core functionality from our components. We strive to limit this approach, but will use it from time to time. ## Classes Aside from our Reboot, a cross-browser normalization stylesheet, all our styles aim to use classes as selectors. This means steering clear of type selectors (e.g., `input[type="text"]`) and extraneous parent classes (e.g., `.parent .child`) that make styles too specific to easily override. As such, components should be built with a base class that houses common, not-to-be overridden property-value pairs. For example, `.btn` and `.btn-primary`. We use `.btn` for all the common styles like `display`, `padding`, and `border-width`. We then use modifiers like `.btn-primary` to add the color, background-color, border-color, etc. Modifier classes should only be used when there are multiple properties or values to be changed across multiple variants. Modifiers are not always necessary, so be sure you’re actually saving lines of code and preventing unnecessary overrides when creating them. Good examples of modifiers are our theme color classes and size variants. ## z-index scales There are two `z-index` scales in Bootstrap—elements within a component and overlay components. ### Component elements - Some components in Bootstrap are built with overlapping elements to prevent double borders without modifying the `border` property. For example, button groups, input groups, and pagination. - These components share a standard `z-index` scale of `0` through `3`. - `0` is default (initial), `1` is `:hover`, `2` is `:active`/`.active`, and `3` is `:focus`. - This approach matches our expectations of highest user priority. If an element is focused, it’s in view and at the user’s attention. Active elements are second highest because they indicate state. Hover is third highest because it indicates user intent, but nearly _anything_ can be hovered. ### Overlay components Bootstrap includes several components that function as an overlay of some kind. This includes, in order of highest `z-index`, dropdowns, fixed and sticky navbars, modals, tooltips, and popovers. These components have their own `z-index` scale that begins at `1000`. This starting number was chosen arbitrarily and serves as a small buffer between our styles and your project’s custom styles. Each overlay component increases its `z-index` value slightly in such a way that common UI principles allow user focused or hovered elements to remain in view at all times. For example, a modal is document blocking (e.g., you cannot take any other action save for the modal’s action), so we put that above our navbars. Learn more about this in our [`z-index` layout page]([[docsref:/layout/z-index]]). ## HTML and CSS over JS Whenever possible, we prefer to write HTML and CSS over JavaScript. In general, HTML and CSS are more prolific and accessible to more people of all different experience levels. HTML and CSS are also faster in your browser than JavaScript, and your browser generally provides a great deal of functionality for you. This principle is our first-class JavaScript API using `data` attributes. You don’t need to write nearly any JavaScript to use our JavaScript plugins; instead, write HTML. Read more about this in [our JavaScript overview page]([[docsref:/getting-started/javascript#data-attributes]]). Lastly, our styles build on the fundamental behaviors of common web elements. Whenever possible, we prefer to use what the browser provides. For example, you can put a `.btn` class on nearly any element, but most elements don’t provide any semantic value or browser functionality. So instead, we use ` `} /> ## File input
      `} /> ## Color Set the `type="color"` and add `.form-control-color` to the ``. We use the modifier class to set fixed `height`s and override some inconsistencies between browsers. Color picker `} /> ## Datalists Datalists allow you to create a group of `