diff --git a/package.json b/package.json index 00387841..109914f1 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,15 @@ "test:update": "npm-run-all charts:test:update icons:test:update lib:test:update" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", "eslint": "8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unicorn": "^51.0.1", - "lerna": "^8.1.2", + "lerna": "^8.1.3", "npm-run-all": "^4.1.5", "prettier": "^3.2.5" }, diff --git a/packages/coreui-react/package.json b/packages/coreui-react/package.json index 90f90185..e66f90bf 100644 --- a/packages/coreui-react/package.json +++ b/packages/coreui-react/package.json @@ -41,31 +41,31 @@ "test:update": "jest --coverage --updateSnapshot" }, "dependencies": { - "@coreui/coreui": "^5.0.0", + "@coreui/coreui": "^5.0.1", "@popperjs/core": "^2.11.8", "prop-types": "^15.8.1" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^14.2.2", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^14.3.1", "@types/jest": "^29.5.12", - "@types/react": "18.2.73", - "@types/react-dom": "^18.2.22", + "@types/react": "18.3.3", + "@types/react-dom": "^18.3.0", "@types/react-transition-group": "^4.4.10", "classnames": "^2.5.1", "cross-env": "^7.0.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-transition-group": "^4.4.5", - "rollup": "^4.13.1", - "ts-jest": "^29.1.2", + "rollup": "^4.18.0", + "ts-jest": "^29.1.3", "tslib": "^2.6.2", - "typescript": "^5.4.3" + "typescript": "^5.4.5" }, "peerDependencies": { "react": ">=17", diff --git a/packages/coreui-react/src/components/dropdown/CDropdown.tsx b/packages/coreui-react/src/components/dropdown/CDropdown.tsx index 303a793a..65101efc 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdown.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdown.tsx @@ -15,10 +15,10 @@ import { PolymorphicRefForwardingComponent } from '../../helpers' import { useForkedRef, usePopper } from '../../hooks' import { placementPropType } from '../../props' import type { Placements } from '../../types' -import { isRTL } from '../../utils' +import { getNextActiveElement, isRTL } from '../../utils' import type { Alignments, Directions } from './types' -import { getNextActiveElement, getPlacement } from './utils' +import { getPlacement } from './utils' export interface CDropdownProps extends HTMLAttributes { /** diff --git a/packages/coreui-react/src/components/dropdown/utils.ts b/packages/coreui-react/src/components/dropdown/utils.ts index edddb0db..c9659636 100644 --- a/packages/coreui-react/src/components/dropdown/utils.ts +++ b/packages/coreui-react/src/components/dropdown/utils.ts @@ -19,28 +19,6 @@ export const getAlignmentClassNames = (alignment: Alignments) => { return classNames } -export const getNextActiveElement = ( - list: HTMLElement[], - activeElement: HTMLElement, - shouldGetNext: boolean, - isCycleAllowed: boolean, -) => { - const listLength = list.length - let index = list.indexOf(activeElement) - - if (index === -1) { - return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0] - } - - index += shouldGetNext ? 1 : -1 - - if (isCycleAllowed) { - index = (index + listLength) % listLength - } - - return list[Math.max(0, Math.min(index, listLength - 1))] -} - export const getPlacement = ( placement: Placement, direction: string | undefined, diff --git a/packages/coreui-react/src/components/tabs/CTab.tsx b/packages/coreui-react/src/components/tabs/CTab.tsx new file mode 100644 index 00000000..a2485297 --- /dev/null +++ b/packages/coreui-react/src/components/tabs/CTab.tsx @@ -0,0 +1,56 @@ +import React, { forwardRef, HTMLAttributes, useContext } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' + +import { TabsContext } from './CTabs' + +export interface CTabProps extends HTMLAttributes { + /** + * A string of all className you want applied to the base component. + */ + className?: string + /** + * Item key. + */ + itemKey?: number | string +} + +export const CTab = forwardRef( + ({ children, className, itemKey, ...rest }, ref) => { + const { _activeItemKey, setActiveKey, id } = useContext(TabsContext) + + const isActive = () => itemKey === _activeItemKey + + return ( + + ) + }, +) + +CTab.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + itemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), +} + +CTab.displayName = 'CTab' diff --git a/packages/coreui-react/src/components/tabs/CTabList.tsx b/packages/coreui-react/src/components/tabs/CTabList.tsx new file mode 100644 index 00000000..7f70938c --- /dev/null +++ b/packages/coreui-react/src/components/tabs/CTabList.tsx @@ -0,0 +1,92 @@ +import React, { forwardRef, HTMLAttributes, KeyboardEvent, useRef } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' + +import { useForkedRef } from '../../hooks' +import { getNextActiveElement } from '../../utils' + +export interface CTabListProps extends HTMLAttributes { + /** + * A string of all className you want applied to the base component. + */ + className?: string + /** + * Specify a layout type for component. + */ + layout?: 'fill' | 'justified' + /** + * Set the nav variant to tabs or pills. + */ + variant?: 'pills' | 'tabs' | 'underline' | 'underline-border' +} + +export const CTabList = forwardRef( + ({ children, className, layout, variant, ...rest }, ref) => { + const tabsRef = useRef(null) + const forkedRef = useForkedRef(ref, tabsRef) + + const handleKeydown = (event: KeyboardEvent) => { + if ( + tabsRef.current !== null && + (event.key === 'ArrowDown' || + event.key === 'ArrowUp' || + event.key === 'ArrowLeft' || + event.key === 'ArrowRight' || + event.key === 'Home' || + event.key === 'End') + ) { + event.preventDefault() + const target = event.target as HTMLElement + // eslint-disable-next-line unicorn/prefer-spread + const items: HTMLElement[] = Array.from( + tabsRef.current.querySelectorAll('.nav-link:not(.disabled):not(:disabled)'), + ) + + let nextActiveElement + + if (event.key === 'Home' || event.key === 'End') { + nextActiveElement = event.key === 'End' ? items.at(-1) : items[0] + } else { + nextActiveElement = getNextActiveElement( + items, + target, + event.key === 'ArrowDown' || event.key === 'ArrowRight', + true, + ) + } + + if (nextActiveElement) { + nextActiveElement.focus({ preventScroll: true }) + } + } + } + + return ( +
+ {children} +
+ ) + }, +) + +CTabList.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + layout: PropTypes.oneOf(['fill', 'justified']), + variant: PropTypes.oneOf(['pills', 'tabs', 'underline', 'underline-border']), +} + +CTabList.displayName = 'CTabList' diff --git a/packages/coreui-react/src/components/tabs/CTabPane.tsx b/packages/coreui-react/src/components/tabs/CTabPane.tsx index 7d66be7f..9cc2e15b 100644 --- a/packages/coreui-react/src/components/tabs/CTabPane.tsx +++ b/packages/coreui-react/src/components/tabs/CTabPane.tsx @@ -18,6 +18,12 @@ export interface CTabPaneProps extends HTMLAttributes { * Callback fired when the component requests to be shown. */ onShow?: () => void + /** + * Enable fade in and fade out transition. + * + * @since 5.1.0 + */ + transition?: boolean /** * Toggle the visibility of component. */ @@ -25,7 +31,7 @@ export interface CTabPaneProps extends HTMLAttributes { } export const CTabPane = forwardRef( - ({ children, className, onHide, onShow, visible, ...rest }, ref) => { + ({ children, className, onHide, onShow, transition = true, visible, ...rest }, ref) => { const tabPaneRef = useRef() const forkedRef = useForkedRef(ref, tabPaneRef) @@ -35,9 +41,9 @@ export const CTabPane = forwardRef(
{ + /** + * A string of all className you want applied to the base component. + */ + className?: string + /** + * Item key. + */ + itemKey?: number | string + /** + * Callback fired when the component requests to be hidden. + */ + onHide?: () => void + /** + * Callback fired when the component requests to be shown. + */ + onShow?: () => void + /** + * Enable fade in and fade out transition. + */ + transition?: boolean + /** + * Toggle the visibility of component. + */ + visible?: boolean +} + +export const CTabPanel = forwardRef( + ({ children, className, itemKey, onHide, onShow, transition = true, visible, ...rest }, ref) => { + const { _activeItemKey, id } = useContext(TabsContext) + + const tabPaneRef = useRef() + const forkedRef = useForkedRef(ref, tabPaneRef) + + const [_visible, setVisible] = useState(visible || _activeItemKey === itemKey) + + useEffect(() => { + visible !== undefined && setVisible(visible) + }, [visible]) + + useEffect(() => { + setVisible(_activeItemKey === itemKey) + }, [_activeItemKey]) + + return ( + + {(state) => ( +
+ {children} +
+ )} +
+ ) + }, +) + +CTabPanel.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + itemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + onHide: PropTypes.func, + onShow: PropTypes.func, + transition: PropTypes.bool, + visible: PropTypes.bool, +} + +CTabPanel.displayName = 'CTabPanel' diff --git a/packages/coreui-react/src/components/tabs/CTabs.tsx b/packages/coreui-react/src/components/tabs/CTabs.tsx new file mode 100644 index 00000000..8740e109 --- /dev/null +++ b/packages/coreui-react/src/components/tabs/CTabs.tsx @@ -0,0 +1,54 @@ +import React, { createContext, forwardRef, HTMLAttributes, useEffect, useId, useState } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' + +export interface CTabsProps extends Omit, 'onChange'> { + /** + * The active item key. + */ + activeItemKey?: number | string + /** + * A string of all className you want applied to the base component. + */ + className?: string + /** + * The callback is fired when the active tab changes. + */ + onChange?: (value: number | string) => void +} + +export interface TabsContextProps { + _activeItemKey?: number | string + setActiveKey: React.Dispatch> + id?: string +} + +export const TabsContext = createContext({} as TabsContextProps) + +export const CTabs = forwardRef( + ({ children, activeItemKey, className, onChange }, ref) => { + const id = useId() + const [_activeItemKey, setActiveKey] = useState(activeItemKey) + + useEffect(() => { + _activeItemKey && onChange && onChange(_activeItemKey) + }, [_activeItemKey]) + + return ( + +
+ {children} +
+
+ ) + }, +) + +CTabs.propTypes = { + activeItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + children: PropTypes.node, + className: PropTypes.string, + onChange: PropTypes.func, +} + +CTabs.displayName = 'CTabs' diff --git a/packages/coreui-react/src/components/tabs/index.ts b/packages/coreui-react/src/components/tabs/index.ts index 1962780c..0d2c6e06 100644 --- a/packages/coreui-react/src/components/tabs/index.ts +++ b/packages/coreui-react/src/components/tabs/index.ts @@ -1,4 +1,8 @@ +import { CTab } from './CTab' import { CTabContent } from './CTabContent' import { CTabPane } from './CTabPane' +import { CTabPanel } from './CTabPanel' +import { CTabList } from './CTabList' +import { CTabs } from './CTabs' -export { CTabContent, CTabPane } +export { CTab, CTabContent, CTabList, CTabPane, CTabPanel, CTabs } diff --git a/packages/coreui-react/src/utils/getNextActiveElement.ts b/packages/coreui-react/src/utils/getNextActiveElement.ts new file mode 100644 index 00000000..a80293ca --- /dev/null +++ b/packages/coreui-react/src/utils/getNextActiveElement.ts @@ -0,0 +1,23 @@ +const getNextActiveElement = ( + list: HTMLElement[], + activeElement: HTMLElement, + shouldGetNext: boolean, + isCycleAllowed: boolean, +) => { + const listLength = list.length + let index = list.indexOf(activeElement) + + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0] + } + + index += shouldGetNext ? 1 : -1 + + if (isCycleAllowed) { + index = (index + listLength) % listLength + } + + return list[Math.max(0, Math.min(index, listLength - 1))] +} + +export default getNextActiveElement diff --git a/packages/coreui-react/src/utils/index.ts b/packages/coreui-react/src/utils/index.ts index 10dbd154..bcdb558e 100644 --- a/packages/coreui-react/src/utils/index.ts +++ b/packages/coreui-react/src/utils/index.ts @@ -1,4 +1,5 @@ import executeAfterTransition from './executeAfterTransition' +import getNextActiveElement from './getNextActiveElement' import getRTLPlacement from './getRTLPlacement' import getTransitionDurationFromElement from './getTransitionDurationFromElement' import isInViewport from './isInViewport' @@ -6,6 +7,7 @@ import isRTL from './isRTL' export { executeAfterTransition, + getNextActiveElement, getRTLPlacement, getTransitionDurationFromElement, isInViewport, diff --git a/packages/docs/content/api/CTab.api.mdx b/packages/docs/content/api/CTab.api.mdx new file mode 100644 index 00000000..f0ba1fe6 --- /dev/null +++ b/packages/docs/content/api/CTab.api.mdx @@ -0,0 +1,11 @@ + +```jsx +import { CTab } from '@coreui/react' +// or +import CTab from '@coreui/react/src/components/tabs/CTab' +``` + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| **className** | A string of all className you want applied to the base component. | `string` | - | +| **itemKey** | Item key. | `string` \| `number` | - | diff --git a/packages/docs/content/api/CTabList.api.mdx b/packages/docs/content/api/CTabList.api.mdx new file mode 100644 index 00000000..0625b962 --- /dev/null +++ b/packages/docs/content/api/CTabList.api.mdx @@ -0,0 +1,12 @@ + +```jsx +import { CTabList } from '@coreui/react' +// or +import CTabList from '@coreui/react/src/components/tabs/CTabList' +``` + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| **className** | A string of all className you want applied to the base component. | `string` | - | +| **layout** | Specify a layout type for component. | `'fill'` \| `'justified'` | - | +| **variant** | Set the nav variant to tabs or pills. | `'pills'` \| `'tabs'` \| `'underline'` \| `'underline-border'` | - | diff --git a/packages/docs/content/api/CTabPane.api.mdx b/packages/docs/content/api/CTabPane.api.mdx index 9b2755bc..e37cc210 100644 --- a/packages/docs/content/api/CTabPane.api.mdx +++ b/packages/docs/content/api/CTabPane.api.mdx @@ -10,4 +10,5 @@ import CTabPane from '@coreui/react/src/components/tabs/CTabPane' | **className** | A string of all className you want applied to the base component. | `string` | - | | **onHide** | Callback fired when the component requests to be hidden. | `() => void` | - | | **onShow** | Callback fired when the component requests to be shown. | `() => void` | - | +| **transition** **_5.1.0+_** | Enable fade in and fade out transition. | `boolean` | true | | **visible** | Toggle the visibility of component. | `boolean` | - | diff --git a/packages/docs/content/api/CTabPanel.api.mdx b/packages/docs/content/api/CTabPanel.api.mdx new file mode 100644 index 00000000..02745245 --- /dev/null +++ b/packages/docs/content/api/CTabPanel.api.mdx @@ -0,0 +1,15 @@ + +```jsx +import { CTabPanel } from '@coreui/react' +// or +import CTabPanel from '@coreui/react/src/components/tabs/CTabPanel' +``` + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| **className** | A string of all className you want applied to the base component. | `string` | - | +| **itemKey** | Item key. | `string` \| `number` | - | +| **onHide** | Callback fired when the component requests to be hidden. | `() => void` | - | +| **onShow** | Callback fired when the component requests to be shown. | `() => void` | - | +| **transition** | Enable fade in and fade out transition. | `boolean` | true | +| **visible** | Toggle the visibility of component. | `boolean` | - | diff --git a/packages/docs/content/api/CTabs.api.mdx b/packages/docs/content/api/CTabs.api.mdx new file mode 100644 index 00000000..3a31ef16 --- /dev/null +++ b/packages/docs/content/api/CTabs.api.mdx @@ -0,0 +1,12 @@ + +```jsx +import { CTabs } from '@coreui/react' +// or +import CTabs from '@coreui/react/src/components/tabs/CTabs' +``` + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| **activeItemKey** | The active item key. | `string` \| `number` | - | +| **className** | A string of all className you want applied to the base component. | `string` | - | +| **onChange** | The callback is fired when the active tab changes. | `(value: string` \| `number) => void` | - | diff --git a/packages/docs/content/api/CTabsContext.api.mdx b/packages/docs/content/api/CTabsContext.api.mdx new file mode 100644 index 00000000..fc799c7c --- /dev/null +++ b/packages/docs/content/api/CTabsContext.api.mdx @@ -0,0 +1,10 @@ + +```jsx +import { CTabsContext } from '@coreui/react' +// or +import CTabsContext from '@coreui/react/src/components/tabs/CTabsContext' +``` + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| **activeItemKey** | The active item key. | `string` \| `number` | - | diff --git a/packages/docs/content/components/navs-tabs.mdx b/packages/docs/content/components/navs-tabs.mdx index 6060b5d7..86f52b5c 100644 --- a/packages/docs/content/components/navs-tabs.mdx +++ b/packages/docs/content/components/navs-tabs.mdx @@ -656,7 +656,7 @@ return ( ### CSS variables -React cards use local CSS variables on `.nav`, `.nav-tabs`, and `.nav-pills` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. +React navs use local CSS variables on `.nav`, `.nav-tabs`, `.nav-pills`, `.nav-underline` and `.nav-underline-border` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. On the `.nav` base class: @@ -670,6 +670,18 @@ On the `.nav-pills` modifier class: +Added in v5.0.0 + +On the `.nav-underline` modifier class: + + + +Added in v5.0.0 + +On the `.nav-underline-border` modifier class: + + + #### How to use CSS variables ```jsx diff --git a/packages/docs/content/components/progress.mdx b/packages/docs/content/components/progress.mdx index 78b1b9ab..905b7cb8 100644 --- a/packages/docs/content/components/progress.mdx +++ b/packages/docs/content/components/progress.mdx @@ -155,7 +155,7 @@ The striped gradient can also be animated. Add `animated` property to ` diff --git a/packages/docs/content/components/tabs.mdx b/packages/docs/content/components/tabs.mdx new file mode 100644 index 00000000..03fd7a76 --- /dev/null +++ b/packages/docs/content/components/tabs.mdx @@ -0,0 +1,347 @@ +--- +title: React Tabs Components +name: Tabs +description: The CoreUI React Tabs component provides a flexible and accessible way to create tabbed navigation in your application. It supports various styles and configurations to meet different design requirements. +menu: Components +route: /components/tabs +since: 5.1.0 +--- + +import { useState } from 'react' + +import { + CTab, + CTabContent, + CTabList, + CTabPanel, + CTabs, +} from '@coreui/react/src/index' + +## Example + +The basic React tabs example uses the `variant="tabs"` props to generate a tabbed interface. + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +## Available styles + +Change the style of ``'s component with modifiers and utilities. Mix and match as needed, or build your own. + +### Unstyled + +If you don’t provide the `variant` prop, the component will default to a basic style. + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +### Pills + +Take that same code, but use `variant="pills"` instead: + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +### Underline + +Take that same code, but use `variant="underline"` instead: + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +### Underline border + +Take that same code, but use `variant="underline-border"` instead: + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +### Fill and justify + +Force your ``'s contents to extend the full available width one of two modifier classes. To proportionately fill all available space use `layout="fill"`. Notice that all horizontal space is occupied, but not every nav item has the same width. + +```jsx preview + + + Home + Profile tab with longer content + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +For equal-width elements, use `layout="justified"`. All horizontal space will be occupied by nav links, but unlike the `layout="fill"` above, every nav item will be the same width. + +```jsx preview + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +## Accessibility + +Dynamic tabbed interfaces, as described in the [WAI ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel), require `role="tablist"`, `role="tab"`, `role="tabpanel"`, and additional `aria-` attributes in order to convey their structure, functionality and current state to users of assistive technologies (such as screen readers). + +### WAI-ARIA Roles + +- The element that serves as the container for the set of tabs has the role `tablist`. +- Each element that serves as a tab has the role `tab` and is contained within the element with the role `tablist`. +- Each element that contains the content panel for a tab has the role `tabpanel`. +- If the tab list has a visible label, the element with the role `tablist` has `aria-labelledby` set to a value that refers to the labeling element. Otherwise, the `tablist` element has a label provided by `aria-label`. +- Each element with the role `tab` has the property `aria-controls` referring to its associated `tabpanel` element. +- The active tab element has the state `aria-selected` set to `true`, and all other tab elements have it set to `false`. +- Each element with the role `tabpanel` has the property `aria-labelledby` referring to its associated `tab` element. + +Our React Tabs component automatically manages all `role="..."` and `aria-` attributes for accessibility. It also handles the selected state by adding `aria-selected="true"` to the active tab. Additionally, you have the flexibility to manually set these attributes, as shown in the example below: + +```jsx + + + Home + Profile + Contact + Disabled + + + + Home tab content + + + Profile tab content + + + Contact tab content + + + Disabled tab content + + + +``` + +This example demonstrates how to manually set `aria-selected`, `aria-controls`, and `aria-labelledby` attributes on your ``'s and ``'s. + +### Keyboard Interaction + +**When focus enters the tab list:** + +Tab: It places focus on the active `tab` element. + +**When focus is on a tab element:** + +Tab: Moves focus to the next element in the tab sequence, typically the tabpanel unless the first focusable element inside the tabpanel is found earlier. + +Left Arrow: Moves focus to the previous tab. If on the first tab, it wraps around to the last tab. + +Right Arrow: Moves focus to the next tab. If on the last tab, it wraps around to the first tab. + +Home: Moves focus to the first tab. + +End: Moves focus to the last tab. + +## Customizing + +### CSS variables + +React tabs use local CSS variables on `.nav`, `.nav-tabs`, `.nav-pills`, `.nav-underline` and `.nav-underline-border` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + +On the `.nav` base class: + + + +On the `.nav-tabs` modifier class: + + + +On the `.nav-pills` modifier class: + + + +On the `.nav-underline` modifier class: + + + +On the `.nav-underline-border` modifier class: + + + +#### How to use CSS variables + +```jsx +const vars = { + '--my-css-var': 10, + '--my-another-css-var': "red" +} +return ... +``` + +### SASS variables + + + +## API + +### CTab + +`markdown:CTab.api.mdx` + +### CTabContent + +`markdown:CTabContent.api.mdx` + +### CTabList + +`markdown:CTabList.api.mdx` + +### CTabPanel + +`markdown:CTabPanel.api.mdx` + +### CTabs + +`markdown:CTabs.api.mdx` + diff --git a/packages/docs/content/getting-started/introduction.mdx b/packages/docs/content/getting-started/introduction.mdx index 679bd183..bf0c7ca2 100644 --- a/packages/docs/content/getting-started/introduction.mdx +++ b/packages/docs/content/getting-started/introduction.mdx @@ -10,35 +10,31 @@ route: /getting-started/introduction ### Npm -```bash +```bash tab={"label":"CoreUI"} npm install @coreui/react @coreui/coreui ``` -If you use CoreUI PRO version. - -```bash +```bash tab={"label":"CoreUI PRO"} npm install @coreui/react-pro @coreui/coreui-pro ``` ### Yarn -```bash +```bash tab={"label":"CoreUI"} yarn add @coreui/react @coreui/coreui ``` -If you use CoreUI PRO version. - -```bash +```bash tab={"label":"CoreUI PRO"} yarn add @coreui/react-pro @coreui/coreui-pro ``` - ## Using components -```jsx +```jsx tab={"label":"CoreUI"} import { CAlert } from '@coreui/react'; +``` -// CoreUI PRO version +```jsx tab={"label":"CoreUI PRO"} import { CAlert } from '@coreui/react-pro'; ``` @@ -50,10 +46,11 @@ React components are styled using the `@coreui/coreui` or `@coreui/coreui-pro` C ###### Basic usage -```js +```js tab={"label":"CoreUI"} import '@coreui/coreui/dist/css/coreui.min.css' +``` -// CoreUI PRO version +```js tab={"label":"CoreUI PRO"} import '@coreui/coreui-pro/dist/css/coreui.min.css' ``` diff --git a/packages/docs/gatsby-browser.js b/packages/docs/gatsby-browser.tsx similarity index 100% rename from packages/docs/gatsby-browser.js rename to packages/docs/gatsby-browser.tsx diff --git a/packages/docs/gatsby-config.js b/packages/docs/gatsby-config.mjs similarity index 88% rename from packages/docs/gatsby-config.js rename to packages/docs/gatsby-config.mjs index 5af7a215..93254870 100644 --- a/packages/docs/gatsby-config.js +++ b/packages/docs/gatsby-config.mjs @@ -1,4 +1,12 @@ -module.exports = { +import remarkGfm from 'remark-gfm' +import remarkCodeTabs from 'remark-code-tabs' + +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const config = { siteMetadata: { title: `CoreUI for React.js`, titleTemplate: `%s · React UI Components · CoreUI `, @@ -52,10 +60,7 @@ module.exports = { resolve: `gatsby-plugin-mdx`, options: { mdxOptions: { - remarkPlugins: [ - // Add GitHub Flavored Markdown (GFM) support - require(`remark-gfm`), - ], + remarkPlugins: [remarkGfm, remarkCodeTabs], }, gatsbyRemarkPlugins: [ { @@ -99,3 +104,5 @@ module.exports = { }, ], } + +export default config diff --git a/packages/docs/gatsby-node.js b/packages/docs/gatsby-node.mjs similarity index 73% rename from packages/docs/gatsby-node.js rename to packages/docs/gatsby-node.mjs index a93ffd30..97f4a8e1 100644 --- a/packages/docs/gatsby-node.js +++ b/packages/docs/gatsby-node.mjs @@ -1,7 +1,12 @@ -const path = require('node:path') -const { createFilePath } = require('gatsby-source-filesystem') +import { resolve } from 'node:path' +import { createFilePath } from 'gatsby-source-filesystem' -exports.onCreateNode = async ({ node, loadNodeContent, actions: { createNodeField }, getNode }) => { +export const onCreateNode = async ({ + node, + loadNodeContent, + actions: { createNodeField }, + getNode, +}) => { if (node.internal.type === 'Mdx') { const slug = createFilePath({ node, getNode }) @@ -22,7 +27,11 @@ exports.onCreateNode = async ({ node, loadNodeContent, actions: { createNodeFiel } } -exports.createPages = async ({ graphql, actions: { createPage, createRedirect }, reporter }) => { +export const createPages = async ({ + graphql, + actions: { createPage, createRedirect }, + reporter, +}) => { const result = await graphql(` query { allMdx { @@ -50,7 +59,7 @@ exports.createPages = async ({ graphql, actions: { createPage, createRedirect }, posts.forEach((node) => { createPage({ path: node.fields.slug, - component: `${path.resolve(`./src/templates/MdxLayout.tsx`)}?__contentFilePath=${ + component: `${resolve(`./src/templates/MdxLayout.tsx`)}?__contentFilePath=${ node.internal.contentFilePath }`, context: { id: node.id }, diff --git a/packages/docs/gatsby-ssr.js b/packages/docs/gatsby-ssr.tsx similarity index 100% rename from packages/docs/gatsby-ssr.js rename to packages/docs/gatsby-ssr.tsx diff --git a/packages/docs/package.json b/packages/docs/package.json index e8b587d9..ab551842 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -25,21 +25,21 @@ }, "dependencies": { "@coreui/chartjs": "^4.0.0", - "@coreui/coreui": "^5.0.0", + "@coreui/coreui": "^5.0.1", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "^2.2.1", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", "@docsearch/css": "^3.6.0", - "@mdx-js/mdx": "^2.3.0", - "@mdx-js/react": "^2.3.0", + "@mdx-js/mdx": "^3.0.1", + "@mdx-js/react": "^3.0.1", "@types/react-helmet": "^6.1.11", - "gatsby": "^5.13.3", + "gatsby": "^5.13.5", "gatsby-plugin-google-tagmanager": "^5.13.1", "gatsby-plugin-image": "^3.13.1", "gatsby-plugin-manifest": "^5.13.1", "gatsby-plugin-mdx": "^5.13.1", - "gatsby-plugin-offline": "^6.13.1", + "gatsby-plugin-offline": "^6.13.2", "gatsby-plugin-react-helmet": "^6.13.1", "gatsby-plugin-sass": "^6.13.1", "gatsby-plugin-sharp": "^5.13.1", @@ -53,13 +53,13 @@ "prism-react-renderer": "^2.3.1", "prismjs": "^1.29.0", "prop-types": "^15.8.1", - "react": "^18.2.0", + "react": "^18.3.1", "react-docgen-typescript": "^2.2.2", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-helmet": "^6.1.0", - "react-imask": "^7.5.0", - "rimraf": "^5.0.5", - "sass": "^1.72.0" + "react-imask": "^7.6.1", + "rimraf": "^5.0.7", + "sass": "^1.77.2" }, "devDependencies": { "npm-run-all": "^4.1.5" diff --git a/packages/docs/src/components/ScssDocs.tsx b/packages/docs/src/components/ScssDocs.tsx index 45d649db..53c36b9f 100644 --- a/packages/docs/src/components/ScssDocs.tsx +++ b/packages/docs/src/components/ScssDocs.tsx @@ -43,7 +43,7 @@ const ScssDocs = ({ file, capture }: ScssDocsProps) => { return ( code && ( -
+
{ } const navGroup = (item: NavItem, index: number) => { - const { name, icon, ...rest } = item + const { name, icon, items } = item return ( - - {item.items?.map((item: NavItem, index: number) => + + {items?.map((item: NavItem, index: number) => item.items ? navGroup(item, index) : navItem(item, index), )} diff --git a/packages/docs/src/components/Toc.tsx b/packages/docs/src/components/Toc.tsx index f67061b8..ad99fa73 100644 --- a/packages/docs/src/components/Toc.tsx +++ b/packages/docs/src/components/Toc.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react' -import { CNav } from '@coreui/react/src/index' +import React, { FC, useState } from 'react' +import { CCollapse, CNav } from '@coreui/react/src/index' type TocItem = { url: string @@ -11,34 +11,58 @@ interface TocProps { items: TocItem[] } -const Toc: FC = ({ items }) => { - const toc = (items: TocItem[]) => { - return ( - items && - items.map((item, index) => { - if (Array.isArray(item.items)) { - return ( -
  • - {item.title} -
      {toc(item.items)}
    -
  • - ) - } +const toc = (items: TocItem[]) => { + return ( + items && + items.map((item, index) => { + if (Array.isArray(item.items)) { return (
  • {item.title} +
      {toc(item.items)}
  • ) - }) - ) - } + } + return ( +
  • + {item.title} +
  • + ) + }) + ) +} +const Toc: FC = ({ items }) => { + const [visible, setVisible] = useState(false) return (
    - On this page - -
      {toc(items)}
    -
    + + On this page + + +
      {toc(items)}
    +
    +
    ) } diff --git a/packages/docs/src/data/other_frameworks.json b/packages/docs/src/data/other_frameworks.json index 32046bbd..df64148b 100644 --- a/packages/docs/src/data/other_frameworks.json +++ b/packages/docs/src/data/other_frameworks.json @@ -1,138 +1,138 @@ { "accordion": { - "angular": "https://coreui.io/angular/docs/components/accordion", + "angular": "https://coreui.io/angular/docs/components/accordion/", "bootstrap": "https://coreui.io/docs/components/accordion/", "react": "https://coreui.io/react/docs/components/accordion/", "vue": "https://coreui.io/vue/docs/components/accordion.html" }, "alert": { - "angular": "https://coreui.io/angular/docs/components/alert", + "angular": "https://coreui.io/angular/docs/components/alert/", "bootstrap": "https://coreui.io/docs/components/alerts/", "react": "https://coreui.io/react/docs/components/alert/", "vue": "https://coreui.io/vue/docs/components/alert.html" }, "avatar": { - "angular": "https://coreui.io/angular/docs/components/avatar", + "angular": "https://coreui.io/angular/docs/components/avatar/", "bootstrap": "https://coreui.io/docs/components/avatar/", "react": "https://coreui.io/react/docs/components/avatar/", "vue": "https://coreui.io/vue/docs/components/avatar.html" }, "badge": { - "angular": "https://coreui.io/angular/docs/components/badge", + "angular": "https://coreui.io/angular/docs/components/badge/", "bootstrap": "https://coreui.io/docs/components/badge/", "react": "https://coreui.io/react/docs/components/badge/", "vue": "https://coreui.io/vue/docs/components/badge.html" }, "breadcrumb": { - "angular": "https://coreui.io/angular/docs/components/breadcrumb", + "angular": "https://coreui.io/angular/docs/components/breadcrumb/", "bootstrap": "https://coreui.io/docs/components/breadcrumb/", "react": "https://coreui.io/react/docs/components/breadcrumb/", "vue": "https://coreui.io/vue/docs/components/breadcrumb.html" }, "button": { - "angular": "https://coreui.io/angular/docs/components/button", + "angular": "https://coreui.io/angular/docs/components/button/", "bootstrap": "https://coreui.io/docs/components/buttons/", "react": "https://coreui.io/react/docs/components/button/", "vue": "https://coreui.io/vue/docs/components/button.html" }, "button-group": { - "angular": "https://coreui.io/angular/docs/components/button-group", + "angular": "https://coreui.io/angular/docs/components/button-group/", "bootstrap": "https://coreui.io/docs/components/button-group/", "react": "https://coreui.io/react/docs/components/button-group/", "vue": "https://coreui.io/vue/docs/components/button-group.html" }, "callout": { - "angular": "https://coreui.io/angular/docs/components/callout", + "angular": "https://coreui.io/angular/docs/components/callout/", "bootstrap": "https://coreui.io/docs/components/callout/", "react": "https://coreui.io/react/docs/components/callout/", "vue": "https://coreui.io/vue/docs/components/callout.html" }, "card": { - "angular": "https://coreui.io/angular/docs/components/card", + "angular": "https://coreui.io/angular/docs/components/card/", "bootstrap": "https://coreui.io/docs/components/card/", "react": "https://coreui.io/react/docs/components/card/", "vue": "https://coreui.io/vue/docs/components/card.html" }, "carousel": { - "angular": "https://coreui.io/angular/docs/components/carousel", + "angular": "https://coreui.io/angular/docs/components/carousel/", "bootstrap": "https://coreui.io/docs/components/carousel/", "react": "https://coreui.io/react/docs/components/carousel/", "vue": "https://coreui.io/vue/docs/components/carousel.html" }, "checkbox": { - "angular": "https://coreui.io/angular/docs/forms/checks-radios", + "angular": "https://coreui.io/angular/docs/forms/checks-radios/", "bootstrap": "https://coreui.io/docs/forms/checks-radios/", "react": "https://coreui.io/react/docs/forms/checkbox/", "vue": "https://coreui.io/vue/docs/forms/checkbox.html" }, "close-button": { - "angular": "https://coreui.io/angular/docs/components/close-button", + "angular": "https://coreui.io/angular/docs/components/close-button/", "bootstrap": "https://coreui.io/docs/components/close-button/", "react": "https://coreui.io/react/docs/components/close-button/", "vue": "https://coreui.io/vue/docs/components/close-button.html" }, "collapse": { - "angular": "https://coreui.io/angular/docs/components/collapse", + "angular": "https://coreui.io/angular/docs/components/collapse/", "bootstrap": "https://coreui.io/docs/components/collapse/", "react": "https://coreui.io/react/docs/components/collapse/", "vue": "https://coreui.io/vue/docs/components/collapse.html" }, "dropdown": { - "angular": "https://coreui.io/angular/docs/components/dropdown", + "angular": "https://coreui.io/angular/docs/components/dropdown/", "bootstrap": "https://coreui.io/docs/components/dropdowns/", "react": "https://coreui.io/react/docs/components/dropdown/", "vue": "https://coreui.io/vue/docs/components/dropdown.html" }, "footer": { - "angular": "https://coreui.io/angular/docs/components/footer", + "angular": "https://coreui.io/angular/docs/components/footer/", "bootstrap": "https://coreui.io/docs/components/footer/", "react": "https://coreui.io/react/docs/components/footer/", "vue": "https://coreui.io/vue/docs/components/footer.html" }, "header": { - "angular": "https://coreui.io/angular/docs/components/header", + "angular": "https://coreui.io/angular/docs/components/header/", "bootstrap": "https://coreui.io/docs/components/header/", "react": "https://coreui.io/react/docs/components/header/", "vue": "https://coreui.io/vue/docs/components/header.html" }, "icon": { - "angular": "https://coreui.io/angular/docs/components/icon", + "angular": "https://coreui.io/angular/docs/components/icon/", "bootstrap": "https://coreui.io/docs/components/icon/", "react": "https://coreui.io/react/docs/components/icon/", "vue": "https://coreui.io/vue/docs/components/icon.html" }, "image": { - "angular": "https://coreui.io/angular/docs/components/image", + "angular": "https://coreui.io/angular/docs/components/image/", "bootstrap": "https://coreui.io/docs/content/images/", "react": "https://coreui.io/react/docs/components/image/", "vue": "https://coreui.io/vue/docs/components/image.html" }, "input": { - "angular": "https://coreui.io/angular/docs/forms/input", + "angular": "https://coreui.io/angular/docs/forms/input/", "bootstrap": "https://coreui.io/docs/forms/form-control/", "react": "https://coreui.io/react/docs/forms/input/", "vue": "https://coreui.io/vue/docs/forms/input.html" }, "input-group": { - "angular": "https://coreui.io/angular/docs/forms/input-group", + "angular": "https://coreui.io/angular/docs/forms/input-group/", "bootstrap": "https://coreui.io/docs/forms/input-group/", "react": "https://coreui.io/react/docs/forms/input-group/", "vue": "https://coreui.io/vue/docs/forms/input-group.html" }, "floating-labels": { - "angular": "https://coreui.io/angular/docs/forms/floating-labels", + "angular": "https://coreui.io/angular/docs/forms/floating-labels/", "bootstrap": "https://coreui.io/docs/forms/floating-labels/", "react": "https://coreui.io/react/docs/forms/floating-labels/", "vue": "https://coreui.io/vue/docs/forms/floating-labels.html" }, "list-group": { - "angular": "https://coreui.io/angular/docs/components/list-group", + "angular": "https://coreui.io/angular/docs/components/list-group/", "bootstrap": "https://coreui.io/docs/components/list-group/", "react": "https://coreui.io/react/docs/components/list-group/", "vue": "https://coreui.io/vue/docs/components/list-group.html" }, "modal": { - "angular": "https://coreui.io/angular/docs/components/modal", + "angular": "https://coreui.io/angular/docs/components/modal/", "bootstrap": "https://coreui.io/docs/components/modal/", "react": "https://coreui.io/react/docs/components/modal/", "vue": "https://coreui.io/vue/docs/components/modal.html" @@ -143,97 +143,97 @@ "vue": "https://coreui.io/vue/docs/components/navbar.html" }, "navs-tabs": { - "angular": "https://coreui.io/angular/docs/components/nav", + "angular": "https://coreui.io/angular/docs/components/nav/", "bootstrap": "https://coreui.io/docs/components/navs-tabs/", "react": "https://coreui.io/react/docs/components/navs-tabs/", "vue": "https://coreui.io/vue/docs/components/navs-tabs.html" }, "offcanvas": { - "angular": "https://coreui.io/angular/docs/components/offcanvas", + "angular": "https://coreui.io/angular/docs/components/offcanvas/", "bootstrap": "https://coreui.io/docs/components/offcanvas/", "react": "https://coreui.io/react/docs/components/offcanvas/", "vue": "https://coreui.io/vue/docs/components/offcanvas.html" }, "pagination": { - "angular": "https://coreui.io/angular/docs/components/pagination", + "angular": "https://coreui.io/angular/docs/components/pagination/", "bootstrap": "https://coreui.io/docs/components/pagination/", "react": "https://coreui.io/react/docs/components/pagination/", "vue": "https://coreui.io/vue/docs/components/pagination.html" }, "placeholder": { - "angular": "https://coreui.io/angular/docs/components/placeholder", + "angular": "https://coreui.io/angular/docs/components/placeholder/", "bootstrap": "https://coreui.io/docs/components/placeholders/", "react": "https://coreui.io/react/docs/components/placeholder/", "vue": "https://coreui.io/vue/docs/components/placeholder.html" }, "popover": { - "angular": "https://coreui.io/angular/docs/components/popover", + "angular": "https://coreui.io/angular/docs/components/popover/", "bootstrap": "https://coreui.io/docs/components/popovers/", "react": "https://coreui.io/react/docs/components/popover/", "vue": "https://coreui.io/vue/docs/components/popover.html" }, "progress": { - "angular": "https://coreui.io/angular/docs/components/progress", + "angular": "https://coreui.io/angular/docs/components/progress/", "bootstrap": "https://coreui.io/docs/components/progress/", "react": "https://coreui.io/react/docs/components/progress/", "vue": "https://coreui.io/vue/docs/components/progress.html" }, "radio": { - "angular": "https://coreui.io/angular/docs/forms/checks-radios", + "angular": "https://coreui.io/angular/docs/forms/checks-radios/", "bootstrap": "https://coreui.io/docs/forms/checks-radios/", "react": "https://coreui.io/react/docs/forms/radio/", "vue": "https://coreui.io/vue/docs/forms/radio.html" }, "range": { - "angular": "https://coreui.io/angular/docs/forms/range", + "angular": "https://coreui.io/angular/docs/forms/range/", "bootstrap": "https://coreui.io/docs/forms/range/", "react": "https://coreui.io/react/docs/forms/range/", "vue": "https://coreui.io/vue/docs/forms/range.html" }, "select": { - "angular": "https://coreui.io/angular/docs/forms/select", + "angular": "https://coreui.io/angular/docs/forms/select/", "bootstrap": "https://coreui.io/docs/forms/select/", "react": "https://coreui.io/react/docs/forms/select/", "vue": "https://coreui.io/vue/docs/forms/select.html" }, "sidebar": { - "angular": "https://coreui.io/angular/docs/components/sidebar", + "angular": "https://coreui.io/angular/docs/components/sidebar/", "bootstrap": "https://coreui.io/docs/components/sidebar/", "react": "https://coreui.io/react/docs/components/sidebar/", "vue": "https://coreui.io/vue/docs/components/sidebar.html" }, "spinner": { - "angular": "https://coreui.io/angular/docs/components/spinner", + "angular": "https://coreui.io/angular/docs/components/spinner/", "bootstrap": "https://coreui.io/docs/components/spinners/", "react": "https://coreui.io/react/docs/components/spinner/", "vue": "https://coreui.io/vue/docs/components/spinner.html" }, "switch": { - "angular": "https://coreui.io/angular/docs/forms/checks-radios", + "angular": "https://coreui.io/angular/docs/forms/checks-radios/", "bootstrap": "https://coreui.io/docs/forms/checks-radios/", "react": "https://coreui.io/react/docs/forms/switch/", "vue": "https://coreui.io/vue/docs/forms/switch.html" }, "table": { - "angular": "https://coreui.io/angular/docs/components/table", + "angular": "https://coreui.io/angular/docs/components/table/", "bootstrap": "https://coreui.io/docs/content/tables/", "react": "https://coreui.io/react/docs/components/table/", "vue": "https://coreui.io/vue/docs/components/table.html" }, "textarea": { - "angular": "https://coreui.io/angular/docs/forms/form-control", + "angular": "https://coreui.io/angular/docs/forms/form-control/", "bootstrap": "https://coreui.io/docs/forms/form-control/", "react": "https://coreui.io/react/docs/forms/textarea/", "vue": "https://coreui.io/vue/docs/forms/textarea.html" }, "toast": { - "angular": "https://coreui.io/angular/docs/components/toast", + "angular": "https://coreui.io/angular/docs/components/toast/", "bootstrap": "https://coreui.io/docs/components/toasts/", "react": "https://coreui.io/react/docs/components/toast/", "vue": "https://coreui.io/vue/docs/components/toast.html" }, "tooltip": { - "angular": "https://coreui.io/angular/docs/components/tooltip", + "angular": "https://coreui.io/angular/docs/components/tooltip/", "bootstrap": "https://coreui.io/docs/components/tooltips/", "react": "https://coreui.io/react/docs/components/tooltip/", "vue": "https://coreui.io/vue/docs/components/tooltip.html" diff --git a/packages/docs/src/nav.tsx b/packages/docs/src/nav.tsx index 2b3a062e..6aebda1d 100644 --- a/packages/docs/src/nav.tsx +++ b/packages/docs/src/nav.tsx @@ -293,6 +293,14 @@ const nav = [ name: 'Table', to: '/components/table/', }, + { + name: 'Tabs', + to: '/components/tabs/', + badge: { + color: 'success', + text: 'New', + }, + }, { name: 'Toast', to: '/components/toast/', diff --git a/packages/docs/src/styles/_component-examples.scss b/packages/docs/src/styles/_component-examples.scss index ed1b36f2..3fe0496c 100644 --- a/packages/docs/src/styles/_component-examples.scss +++ b/packages/docs/src/styles/_component-examples.scss @@ -2,6 +2,14 @@ // Docs examples // +.tab-content .tab-pane { + @include border-top-radius(0); + + .highlight { + @include border-top-radius(0); + } +} + .docs-example-snippet { border: solid var(--cui-border-color); border-width: 1px 0; @@ -373,19 +381,20 @@ .highlight { position: relative; padding: .75rem ($cd-gutter-x * .5); - margin-bottom: 1rem; + margin: 0 ($cd-gutter-x * -.5) 1rem ($cd-gutter-x * -.5) ; border: 1px solid var(--cui-border-color); background-color: var(--cd-pre-bg); @include media-breakpoint-up(md) { padding: .75rem 1.25rem; + margin-right: 0; + margin-left: 0; @include border-radius(var(--cui-border-radius)); } pre { padding: .25rem 0 .875rem; margin-top: .8125rem; - margin-right: 1.875rem; margin-bottom: 0; overflow: overlay; white-space: pre; @@ -404,7 +413,7 @@ margin: 0 ($cd-gutter-x * -.5) $spacer; .highlight { - margin-bottom: 0; + margin: 0; } .docs-example ~ .highlight { diff --git a/packages/docs/src/styles/_layout.scss b/packages/docs/src/styles/_layout.scss index 38bfe8c4..2ee63995 100644 --- a/packages/docs/src/styles/_layout.scss +++ b/packages/docs/src/styles/_layout.scss @@ -3,4 +3,52 @@ @include ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); will-change: auto; @include transition(padding .15s); + + > .container-lg { + --cui-gutter-x: 3rem; + } +} + +.docs-sidebar { + grid-area: sidebar; +} + +.docs-main { + grid-area: main; + + @include media-breakpoint-down(lg) { + max-width: 760px; + margin-inline: auto; + } + + @include media-breakpoint-up(md) { + display: grid; + grid-template-areas: + "intro" + "toc" + "content"; + grid-template-rows: auto auto 1fr; + gap: $grid-gutter-width; + } + + @include media-breakpoint-up(lg) { + grid-template-areas: + "intro toc" + "content toc"; + grid-template-rows: auto 1fr; + grid-template-columns: 4fr 1fr; + } +} + +.docs-intro { + grid-area: intro; +} + +.docs-toc { + grid-area: toc; +} + +.docs-content { + grid-area: content; + min-width: 1px; // Fix width when bd-content contains a `
    ` https://github.com/twbs/bootstrap/issues/25410
     }
    diff --git a/packages/docs/src/styles/_toc.scss b/packages/docs/src/styles/_toc.scss
    index 6178d193..8f6dc04d 100644
    --- a/packages/docs/src/styles/_toc.scss
    +++ b/packages/docs/src/styles/_toc.scss
    @@ -39,4 +39,49 @@
           }
         }
       }
    -}
    \ No newline at end of file
    +}
    +
    +.docs-toc-toggle {
    +  display: flex;
    +  align-items: center;
    +
    +  @include media-breakpoint-down(sm) {
    +    justify-content: space-between;
    +    width: 100%;
    +  }
    +
    +  @include media-breakpoint-down(md) {
    +    color: var(--cui-body-color);
    +    border: 1px solid var(--cui-border-color);
    +    @include border-radius(var(--cui-border-radius));
    +
    +    &:hover,
    +    &:focus,
    +    &:active,
    +    &[aria-expanded="true"] {
    +      color: var(--cui-primary);
    +      background-color: var(--cui-body-bg);
    +      border-color: var(--cui-primary);
    +    }
    +
    +    &:focus,
    +    &[aria-expanded="true"] {
    +      box-shadow: 0 0 0 3px rgba(var(--cui-primary-rgb), .25);
    +    }
    +  }
    +}
    +
    +.docs-toc-collapse {
    +  @include media-breakpoint-down(md) {
    +    nav {
    +      padding: 1.25rem 1.25rem 1.25rem 1rem;
    +      background-color: var(--cui-tertiary-bg);
    +      border: 1px solid var(--cui-border-color);
    +      @include border-radius(var(--cui-border-radius));
    +    }
    +  }
    +
    +  @include media-breakpoint-up(md) {
    +    display: block !important; // stylelint-disable-line declaration-no-important
    +  }
    +}
    diff --git a/packages/docs/src/templates/DefaultLayout.tsx b/packages/docs/src/templates/DefaultLayout.tsx
    index 2065aa0e..83c5fd99 100644
    --- a/packages/docs/src/templates/DefaultLayout.tsx
    +++ b/packages/docs/src/templates/DefaultLayout.tsx
    @@ -29,17 +29,15 @@ const DefaultLayout: FC = ({ children, data, pageContext, pa
         >
           
           
    -      
    +
    -
    - {path === '/404/' ? ( - {children} - ) : ( - - {children} - - )} -
    + {path === '/404/' ? ( + {children} + ) : ( + + {children} + + )}