├── .eslintrc.json
├── .github
├── renovate.json
└── workflows
│ ├── deploy.yml
│ ├── release.yml
│ └── update-alpha.yml
├── .gitignore
├── .prettierrc
├── .storybook
├── main.cjs
└── preview-head.html
├── @types
└── index.d.ts
├── CHANGELOG.md
├── CNAME
├── LICENSE
├── README.md
├── docs
├── pages
│ ├── getting-started.mdx
│ └── index.jsx
└── src
│ ├── Main.tsx
│ ├── components
│ ├── Code.css
│ ├── Code.tsx
│ ├── PropTable.module.css
│ └── PropTable.tsx
│ └── plugins
│ ├── jsxExample.cjs
│ └── propTable.cjs
├── esbuild.mjs
├── index.html
├── package-lock.json
├── package.json
├── packages
├── _helpers
│ ├── affix.tsx
│ ├── clickable.tsx
│ ├── dead-toggle.tsx
│ ├── expand-transition.tsx
│ ├── index.ts
│ ├── props.ts
│ └── unstyled-heading.tsx
├── alert
│ ├── docs
│ │ └── Alert.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Alert.stories.tsx
├── attention
│ ├── docs
│ │ └── Attention.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.ts
│ └── stories
│ │ └── Attention.stories.tsx
├── box
│ ├── docs
│ │ └── Box.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Box.stories.tsx
├── breadcrumbs
│ ├── docs
│ │ └── Breadcrumbs.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Breadcrumbs.stories.tsx
├── button
│ ├── docs
│ │ └── Button.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Button.stories.tsx
├── card
│ ├── docs
│ │ └── Card.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Card.stories.tsx
├── combobox
│ ├── docs
│ │ └── Combobox.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── props.ts
│ │ └── utils.ts
│ └── stories
│ │ └── Combobox.stories.tsx
├── expandable
│ ├── docs
│ │ └── Expandable.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Expandable.stories.tsx
├── index.ts
├── modal
│ ├── docs
│ │ └── Modal.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.ts
│ └── stories
│ │ └── Modal.stories.tsx
├── pill
│ ├── docs
│ │ └── Pill.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Button.stories.tsx
├── select
│ ├── __tests__
│ │ └── Select.test.tsx
│ ├── docs
│ │ └── Select.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Select.stories.tsx
├── slider
│ ├── docs
│ │ └── Slider.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.ts
│ └── stories
│ │ └── Slider.stories.tsx
├── steps
│ ├── docs
│ │ └── Steps.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── props.tsx
│ │ └── step.tsx
│ └── stories
│ │ └── Steps.stories.tsx
├── switch
│ ├── docs
│ │ └── Switch.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.tsx
│ └── stories
│ │ └── Switch.stories.tsx
├── tabs
│ ├── __tests__
│ │ ├── Tabs.test.tsx
│ │ └── __snapshots__
│ │ │ └── Tabs.test.tsx.snap
│ ├── docs
│ │ └── Tabs.mdx
│ ├── src
│ │ ├── component-tab-panel.tsx
│ │ ├── component-tab.tsx
│ │ ├── component-tabs.tsx
│ │ ├── index.ts
│ │ ├── props.ts
│ │ └── utils.ts
│ └── stories
│ │ └── Tabs.stories.tsx
├── textarea
│ ├── __tests__
│ │ └── TextArea.test.tsx
│ ├── docs
│ │ └── TextArea.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── props.ts
│ │ └── useTextAreaHeight.ts
│ └── stories
│ │ └── TextArea.stories.tsx
├── textfield
│ ├── __tests__
│ │ └── Textfield.test.tsx
│ ├── docs
│ │ └── TextField.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ └── props.ts
│ └── stories
│ │ └── Textfield.stories.tsx
├── toggle
│ ├── docs
│ │ └── Toggle.mdx
│ ├── src
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── item.tsx
│ │ └── props.ts
│ └── stories
│ │ ├── Checkbox.stories.tsx
│ │ ├── Radio.stories.tsx
│ │ └── RadioButtons.stories.tsx
└── utils
│ └── src
│ ├── index.ts
│ ├── useElementSizeObserver.ts
│ ├── useId.ts
│ ├── useIsomorphicLayoutEffect.ts
│ └── useLogDeprecationWarning.ts
├── release.config.cjs
├── tests
├── eik-react-jsx
│ ├── README.md
│ ├── index.html
│ ├── index.jsx
│ └── package.json
├── eik-react
│ ├── README.md
│ ├── index.html
│ ├── index.js
│ └── package.json
├── ssr-react-16-cjs
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── client.jsx
│ │ ├── index.jsx
│ │ └── server.js
├── ssr-react-16
│ ├── README.md
│ ├── client.jsx
│ ├── index.jsx
│ ├── package.json
│ └── server.js
├── ssr-react-ts-16-cjs
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── client.tsx
│ │ ├── index.tsx
│ │ └── server.ts
└── ssr-react-ts-16
│ ├── README.md
│ ├── package.json
│ └── src
│ ├── client.tsx
│ ├── index.tsx
│ └── server.ts
├── tsconfig.json
└── vite.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app"],
3 | "rules": {
4 | // Disabling this rule as we sometimes create empty links for example purposes
5 | "jsx-a11y/anchor-is-valid": 0,
6 | // Disabling this rule as we mostly pass props forward (including children)
7 | "jsx-a11y/heading-has-content": 0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>podium-lib/renovate-presets:top-level-module"],
4 | "baseBranches": ["next"],
5 | "lockFileMaintenance": {
6 | "automerge": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy documentation to GitHub Pages and publish package
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 | - name: Install Dependencies
15 | run: |
16 | npm install
17 | - name: Build Packages
18 | run: |
19 | npm run build
20 | - name: Build Docs
21 | run: |
22 | npm run docs:build
23 | - name: Deploy
24 | if: success()
25 | uses: crazy-max/ghaction-github-pages@v3
26 | with:
27 | target_branch: gh-pages
28 | build_dir: site
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 |
9 | jobs:
10 | release:
11 | name: Release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: '16.x'
22 | - name: Install dependencies
23 | run: npm install
24 | - name: Build
25 | run: npm run build
26 | - name: Release
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
31 | run: npx semantic-release
32 | - name: Eik login and publish
33 | run: npm run eik:login -- -k $EIK_TOKEN && npm run eik:publish || true
34 | env:
35 | EIK_TOKEN: ${{ secrets.EIK_TOKEN }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/update-alpha.yml:
--------------------------------------------------------------------------------
1 | name: Update next
2 | on:
3 | workflow_run:
4 | workflows:
5 | - Release
6 | branches:
7 | - main
8 | types: completed
9 | jobs:
10 | rebase:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 | - name: Rebase next to main
16 | run: |
17 | git fetch --unshallow
18 | git checkout next
19 | git rebase origin/main
20 | git push
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | yarn.lock
4 | yarn-error.log
5 |
6 | # doc site build
7 | site/
8 |
9 | dist/
10 | .eik
11 |
12 | *.log
13 | *.swp
14 |
15 | # editors
16 | .idea/
17 | .vscode/
18 |
19 | # os
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "proseWrap": "always",
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "tabWidth": 2,
6 | "overrides": [
7 | {
8 | "files": "*.css",
9 | "options": {
10 | "singleQuote": false
11 | }
12 | },
13 | {
14 | "files": "*.json",
15 | "options": {
16 | "tabWidth": 2
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.storybook/main.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | core: {
3 | builder: 'webpack5',
4 | },
5 | stories: ['../packages/**/*.stories.[tj]sx'],
6 | addons: ['@storybook/addon-actions/register', '@storybook/addon-postcss'],
7 | framework: '@storybook/react',
8 | webpackFinal: async (config) => {
9 | config.resolve.alias['@fabric-ds/core/attention'] = require.resolve(
10 | '../node_modules/@fabric-ds/core/dist/attention/index.js',
11 | );
12 | config.module.rules.push({
13 | test: /\.js$/,
14 | include: /node_modules/,
15 | use: [
16 | {
17 | loader: require.resolve('babel-loader'),
18 | options: {
19 | presets: [require('@babel/preset-env').default],
20 | },
21 | },
22 | ],
23 | });
24 | config.resolve.extensions.push('.js');
25 | return config;
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/@types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.mdx' {
2 | const content: React.FunctionComponent<{}>;
3 | export default content;
4 | }
5 |
6 | declare module '*.module.css' {
7 | const classes: { [key: string]: string };
8 | export default classes;
9 | }
10 |
11 | declare module '@fabric-ds/css';
12 | declare module '@fabric-ds/css/component-classes';
13 | declare module '@fabric-ds/css/tailwind-css';
14 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | react.fabric-ds.io
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 FINN.no
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fabric React
2 |
3 | Reacts components exported under `@fabric-ds/react`.
4 |
5 | ## Development
6 |
7 | The project uses [Storybook](https://storybook.js.org/) for component
8 | development. Start the storybook instance by running the following command:
9 |
10 | ```sh
11 | yarn dev
12 | ```
13 |
14 | ## Documentation
15 |
16 | To start a local dev server for the documentation site, run the following
17 | command:
18 |
19 | ```sh
20 | yarn docs:dev
21 | ```
22 |
23 | ## Releases
24 |
25 | This project uses
26 | [Semantic Release](https://github.com/semantic-release/semantic-release) to
27 | automate package publishing when making changes to the `main` or `next` branch.
28 |
29 | It is recommended to branch off the `next` branch and follow
30 | [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
31 | when making changes. When your changes are ready for pull request, this should
32 | be opened against the `next` branch.
33 |
34 | [Read more in-depth about Fabric Releases here](https://github.com/fabric-ds/issues/blob/779d59723993c13d62374516259602d967da56ca/rfcs/0004-releases.md).
35 |
36 | Please note that the version published will depend on your commit message
37 | structure. We use [commitizen](https://github.com/commitizen/cz-cli) to help
38 | follow this structure:
39 |
40 | ```
41 | npm install -g commitizen
42 | ```
43 |
44 | When installed, you should be able to type `cz` or `git cz` in your terminal to
45 | commit your changes (replacing `git commit`).
46 |
47 | [](https://github.com/commitizen/cz-cli/raw/master/meta/screenshots/add-commit.png)
48 |
--------------------------------------------------------------------------------
/docs/pages/getting-started.mdx:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | This page describes how to get started building an application with Fabric
4 | React.
5 |
6 | ## Setting up your app
7 |
8 | Ensure the [Fabric CSS](https://css.fabric-ds.io) stylesheet is loaded in the
9 | page you are working on.
10 |
11 | See the
12 | [Fabric CSS getting started guide](https://css.fabric-ds.io/pages/getting-started.html)
13 | for more information.
14 |
15 | ## Installation
16 |
17 | All the Fabric React code is provided as a single package and each component can
18 | be imported from this package.
19 |
20 | ### Install from NPM
21 |
22 | The Fabric React package can be installed from NPM.
23 |
24 | ```shell
25 | npm install @fabric-ds/react
26 | ```
27 |
28 | ### Install from Eik
29 |
30 | The same package is also available via our Eik CDN server
31 |
32 | ```html
33 | https://assets.finn.no/pkg/@fabric-ds/react/v0/index.js
34 | ```
35 |
36 | ### Importing Components
37 |
38 | Once installed, components can be imported into your app by name. Below are
39 | examples of how thats done
40 |
41 |
42 |
43 | #### Importing from the NPM package
44 |
45 | 👉 _**This is the most common method and should be used in most cases**_
46 |
47 | When importing from NPM you will need to ensure you have build tooling in place.
48 | If you are working with Podium podlets or layouts which is the most common use
49 | case at Finn, you likely already have Eik in place with Rollup or Esbuild in
50 | which case no further action should be needed.
51 |
52 | If not, take a look at the React
53 | [getting started](https://reactjs.org/docs/getting-started.html) docs.
54 |
55 | _Example_
56 |
57 | ```js
58 | import { Button } from '@fabric-ds/react';
59 | ```
60 |
61 |
62 |
63 | #### Importing directly from Eik
64 |
65 | 👉 _**This is great for prototyping (can also be used in production)**_
66 |
67 | It is also possible to import components directly from the URL on our Eik
68 | server. While not common, it should be possible to write React code without the
69 | need for a build setup. You might find this useful for rapid prototyping
70 | something on a hack day for example!
71 |
72 | _Example_
73 |
74 | ```js
75 | import { Button } from 'https://assets.finn.no/pkg/@fabric-ds/react/v0/index.js';
76 | ```
77 |
78 | If you go down this route, other dependencies such as React and React Dom will
79 | also need to be imported via URLs. These can also be found on the Eik server.
80 |
81 | _Example_
82 |
83 | ```js
84 | import React from 'https://assets.finn.no/npm/@pika/react/v16/index.js';
85 | import ReactDom from 'https://assets.finn.no/npm/@pika/react-dom/v16/index.js';
86 | ```
87 |
88 |
89 |
90 | #### Individual component imports
91 |
92 | You can find the specific import statement to import each component on that
93 | component's documentation page. For example, here's the [button page](/button)
94 |
95 | ## TypeScript support
96 |
97 | The components are written in TypeScript. To take advantage of this, make sure
98 | your project is up to date on the latest `@types/react` definitions.
99 |
100 | ```shell
101 | # YARN
102 | yarn add @types/react -D
103 |
104 | # NPM
105 | npm i @types/react -D
106 | ```
107 |
--------------------------------------------------------------------------------
/docs/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GettingStarted from './getting-started.mdx';
3 |
4 | export default function Index() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/docs/src/Main.tsx:
--------------------------------------------------------------------------------
1 | import { MDXProvider } from '@mdx-js/react';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import {
5 | BrowserRouter as Router,
6 | Route,
7 | Switch as ReactSwitch,
8 | } from 'react-router-dom';
9 | import Alert from '../../packages/alert/docs/Alert.mdx';
10 | import Box from '../../packages/box/docs/Box.mdx';
11 | import Breadcrumbs from '../../packages/breadcrumbs/docs/Breadcrumbs.mdx';
12 | import Button from '../../packages/button/docs/Button.mdx';
13 | import Pill from '../../packages/pill/docs/Pill.mdx';
14 | import Card from '../../packages/card/docs/Card.mdx';
15 | import Combobox from '../../packages/combobox/docs/Combobox.mdx';
16 | import Expandable from '../../packages/expandable/docs/Expandable.mdx';
17 | import Modal from '../../packages/modal/docs/Modal.mdx';
18 | import Select from '../../packages/select/docs/Select.mdx';
19 | import Slider from '../../packages/slider/docs/Slider.mdx';
20 | import Steps from '../../packages/steps/docs/Steps.mdx';
21 | import Switch from '../../packages/switch/docs/Switch.mdx';
22 | import Tabs from '../../packages/tabs/docs/Tabs.mdx';
23 | import TextArea from '../../packages/textarea/docs/TextArea.mdx';
24 | import TextField from '../../packages/textfield/docs/TextField.mdx';
25 | import Toggle from '../../packages/toggle/docs/Toggle.mdx';
26 | import Attention from '../../packages/attention/docs/Attention.mdx';
27 | import Home from '../pages/index.jsx';
28 | import Code from './components/Code';
29 | import PropTable from './components/PropTable';
30 |
31 | const components = {
32 | code: Code,
33 | PropTable,
34 | pre: (props) =>
,
35 | img: ({ style, ...props }) => (
36 | // eslint-disable-next-line jsx-a11y/alt-text
37 |
38 | ),
39 | };
40 |
41 | const App = () => {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | render(App(), document.querySelector('#root'));
128 |
--------------------------------------------------------------------------------
/docs/src/components/Code.css:
--------------------------------------------------------------------------------
1 | .prism-code {
2 | font-size: 14px;
3 | overflow-x: auto;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/src/components/Code.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Highlight, { defaultProps } from 'prism-react-renderer';
3 | import theme from 'prism-react-renderer/themes/github';
4 | import './Code.css';
5 |
6 | const Code = ({ children, className, ...props }) => {
7 | const language = className.replace(/language-/, '');
8 |
9 | return (
10 |
16 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
17 |
21 | {tokens.map((line, i) => (
22 |
23 | {line.map((token, key) => (
24 |
25 | ))}
26 |
27 | ))}
28 |
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default Code;
35 |
--------------------------------------------------------------------------------
/docs/src/components/PropTable.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | border-collapse: collapse;
3 | font-size: 14px;
4 | margin-bottom: 2rem;
5 | width: 100%;
6 | }
7 |
8 | .table tr {
9 | border-bottom: 1px solid #ddd;
10 | }
11 |
12 | .table th {
13 | text-transform: uppercase;
14 | font-size: 11px;
15 | text-align: left;
16 | color: #767676;
17 | }
18 |
19 | .table td,
20 | .table th {
21 | padding: 10px 16px;
22 | vertical-align: top;
23 | }
24 |
25 | /** monospace fonts for all columns except the description column */
26 | .table td:not(:last-child) {
27 | font-family: "Monaco", "Ubuntu Mono", "Consolas", monospace;
28 | }
29 |
30 | /** links to types should keep their colors */
31 | .table td:not(:last-child) a {
32 | color: inherit;
33 | }
34 |
35 | /** no bottom margin for the last paragraph in the cell */
36 | .table td p:last-child {
37 | margin-bottom: 0;
38 | }
39 |
40 | .name {
41 | color: rgb(0, 0, 159);
42 | }
43 |
44 | .value {
45 | color: rgb(227, 17, 108);
46 | }
47 |
48 | .type {
49 | color: rgb(54, 172, 170);
50 | }
51 |
--------------------------------------------------------------------------------
/docs/src/components/PropTable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './PropTable.module.css';
3 |
4 | interface Prop {
5 | name: string;
6 | description: string;
7 | descriptionHtml: string;
8 | required: boolean;
9 | defaultValue?: {
10 | value?: string | number | boolean;
11 | };
12 | type: {
13 | name: string;
14 | };
15 | }
16 |
17 | type PropTableProps = {
18 | props: { [prop: string]: Prop };
19 | };
20 |
21 | export default function PropTable({ props }: PropTableProps) {
22 | return (
23 |
24 |
25 |
26 |
27 | Name
28 | Type
29 | Default
30 | Description
31 |
32 |
33 |
34 | {Object.values(props).map((prop) => (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
51 | ))}
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | const Name = ({ prop }: { prop: Prop }) => {
59 | let className = styles.name;
60 |
61 | if (prop.description.includes('@deprecated')) {
62 | className += ' line-through';
63 | }
64 |
65 | const name = {prop.name} ;
66 |
67 | // Add asterix after the prop name if it is required
68 | if (prop.required) {
69 | return (
70 | <>
71 | {name}
72 | *
73 | >
74 | );
75 | } else {
76 | return name;
77 | }
78 | };
79 |
80 | const DefaultValue = ({ prop }: { prop: Prop }) => {
81 | const defaultValue = prop.defaultValue;
82 |
83 | if (!defaultValue) return ;
84 |
85 | const value = defaultValue.value;
86 |
87 | if (!value) return - ;
88 |
89 | // if we set the default value using tsdoc, then it comes out as a string,
90 | // so we check especially if the string val is 'false' or 'true'
91 | if (typeof value === 'string' && value !== 'false' && value !== 'true') {
92 | return "{value}" ;
93 | } else return {value.toString()} ;
94 | };
95 |
96 | const Type = ({ prop }: { prop: Prop }) => {
97 | const render = (propTypeType) => {
98 | const url = TYPE_URLS[propTypeType];
99 |
100 | let className = '';
101 | if (propTypeType.startsWith('"')) {
102 | className = styles.value;
103 | }
104 |
105 | if (url)
106 | return (
107 |
108 | {propTypeType}
109 |
110 | );
111 | else {
112 | return {propTypeType} ;
113 | }
114 | };
115 |
116 | const { name: propTypeType } = prop.type;
117 |
118 | // Check if it is a union type
119 | if (propTypeType.includes(' | ')) {
120 | const types = propTypeType.split(' | ');
121 | return (
122 | <>
123 | {types.map((type) => (
124 |
125 | |
126 | {render(type)}
127 |
128 | ))}
129 | >
130 | );
131 | } else {
132 | return render(propTypeType);
133 | }
134 | };
135 |
136 | const TYPE_URLS = {
137 | string:
138 | 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String',
139 | number:
140 | 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number',
141 | boolean:
142 | 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean',
143 | Date: 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date',
144 | CSSProperties: 'https://reactjs.org/docs/dom-elements.html#style',
145 | ReactNode: 'https://reactjs.org/docs/rendering-elements.html',
146 | 'RefObject': 'https://reactjs.org/docs/refs-and-the-dom.html',
147 | };
148 |
--------------------------------------------------------------------------------
/docs/src/plugins/jsxExample.cjs:
--------------------------------------------------------------------------------
1 | const visit = require('unist-util-visit');
2 | const u = require('unist-builder');
3 |
4 | /**
5 | * Remark plugin renders JSX code fences marked `example`
6 | */
7 |
8 | module.exports = () => (tree, file) => {
9 | let statefulExampleCount = 0;
10 |
11 | visit(tree, 'code', (node, index) => {
12 | // We are only interested in code nodes of type jsx which are marked example
13 | if (node.lang === 'jsx' && /^example/.test(node.meta)) {
14 | // MDX doesn't only supports variables as exports.
15 | // So for stateful examples (hooks) we export the example snippet, and render it with jsx
16 |
17 | // FIXME: this is a really poor way of separating interactive examples with state and plain jsx (which works fine in mdx)
18 | if (node.value.includes('function')) {
19 | tree.children.splice(
20 | index + 1,
21 | 0,
22 | u('jsx', {
23 | value: `
`,
24 | }),
25 | );
26 | tree.children.push(
27 | u('export', {
28 | value:
29 | `export const Example${statefulExampleCount} = ` + node.value,
30 | }),
31 | );
32 |
33 | statefulExampleCount = statefulExampleCount + 1;
34 | } else {
35 | // take the content of the code fence, and add it after the current node as a jsx type
36 | tree.children.splice(
37 | index + 1,
38 | 0,
39 | u('jsx', {
40 | value: `${node.value}
`,
41 | }),
42 | );
43 | }
44 | }
45 | });
46 | };
47 |
--------------------------------------------------------------------------------
/docs/src/plugins/propTable.cjs:
--------------------------------------------------------------------------------
1 | const docgen = require('react-docgen-typescript');
2 | const visit = require('unist-util-visit');
3 | const u = require('unist-builder');
4 | const unified = require('unified');
5 | const path = require('path');
6 | const html = require('rehype-stringify');
7 | const remark2rehype = require('remark-rehype');
8 | const markdown = require('remark-parse');
9 |
10 | // currently we are abusing the code fence syntax to create our proptables. This way we don't have to add support for a custom syntax
11 | // eg:
12 | //
13 | // ```props packages/button/src/Button.tsx
14 | // ```
15 |
16 | const docGenOptions = {
17 | // to prevent showing EVERY html prop when for instance a component is a wrapper around an input and the type is defined as { ... } & React.HTMLAttributes>
18 | propFilter: (prop, component) => {
19 | if (prop.parent) {
20 | return (
21 | prop.parent.fileName.includes('react-spring-bottom-sheet') ||
22 | !prop.parent.fileName.includes('node_modules')
23 | );
24 | }
25 |
26 | return true;
27 | },
28 | };
29 |
30 | module.exports = () => (tree, file) => {
31 | const cwd = file.cwd;
32 |
33 | visit(tree, 'code', (node, index) => {
34 | // We are only interested in code nodes of type `props` with a relative path
35 | if (node.lang === 'props' && node.meta) {
36 | let displayName;
37 | let relativePath;
38 | if (node.meta.includes(' ')) {
39 | [relativePath, displayName] = node.meta.split(' ');
40 | } else {
41 | relativePath = node.meta;
42 | }
43 |
44 | // the meta property should be the path to the component file, relative to the root
45 | const componentPath = path.resolve(cwd, relativePath);
46 |
47 | let docs = docgen.parse(componentPath, docGenOptions);
48 |
49 | docs = parseMarkdownDescriptions(docs);
50 |
51 | if (docs.length > 1 && !displayName) {
52 | console.warn(
53 | 'Found multiple prop declarations at path: ' +
54 | componentPath +
55 | '. You need to specify one of ' +
56 | docs.map((comp) => comp.displayName).join(', '),
57 | );
58 | }
59 |
60 | // not sure what happens yet if a component has multiple exports/prop types
61 | let component;
62 | if (displayName) {
63 | component = docs.find(
64 | (d) => d.displayName.toLowerCase() === displayName.toLowerCase(),
65 | );
66 | } else {
67 | component = docs[0];
68 | }
69 |
70 | if (!component) {
71 | console.warn(
72 | 'Unable to generate prop table for component at path: ' +
73 | componentPath +
74 | ' with displayName ' +
75 | displayName,
76 | );
77 | }
78 |
79 | // Special handling for any `as` prop.
80 | // We don't want to list every possible HTML element
81 | if (component && 'as' in component.props) {
82 | component.props.as.type.name = 'string | Component';
83 | }
84 |
85 | // Replace the code fence with our prop table component
86 | // The PropTable component should be made globally available to the MDXProvider (so we don't have to import it in every mdx file)
87 | tree.children[index] = u('jsx', {
88 | value: ` `,
89 | });
90 | }
91 | });
92 | };
93 |
94 | // ts doc comments may include markdown, but react-docgen-typescript returns the description as plain text
95 |
96 | const processor = unified()
97 | .use(markdown, { commonmark: true })
98 | .use(remark2rehype)
99 | .use(html);
100 |
101 | function parseMarkdownDescriptions(docs) {
102 | return docs.map((component) => {
103 | const props = {};
104 |
105 | for (const [propName, propDoc] of Object.entries(component.props)) {
106 | if (propDoc.description) {
107 | const result = processor.processSync(propDoc.description);
108 | propDoc.descriptionHtml = result.contents;
109 | }
110 |
111 | props[propName] = propDoc;
112 | }
113 |
114 | component.props = props;
115 |
116 | return component;
117 | });
118 | }
119 |
--------------------------------------------------------------------------------
/esbuild.mjs:
--------------------------------------------------------------------------------
1 | import { ok } from 'node:assert';
2 | import * as eik from '@eik/esbuild-plugin';
3 | import esbuild from 'esbuild';
4 |
5 | // maps React versions to import map file versions.
6 | // Add more mappings here when new versions of React become available.
7 | const versions = new Map([
8 | ['17', 'v2'],
9 | ['18', 'v3'],
10 | ]);
11 |
12 | const version = process.argv[2];
13 | const reactVersions = Array.from(versions.keys());
14 | ok(
15 | reactVersions.includes(version),
16 | `Version argument is required. Must be one of: ${reactVersions.join(
17 | ',',
18 | )}. Eg. 'node esbuild.mjs 18'`,
19 | );
20 |
21 | await eik.load({
22 | urls: [`https://assets.finn.no/map/react/${versions.get(version)}`],
23 | });
24 |
25 | // legacy support for older filenames
26 | if (version === '17') {
27 | await esbuild.build({
28 | plugins: [eik.plugin()],
29 | entryPoints: ['packages/index.ts'],
30 | bundle: true,
31 | outfile: `dist/eik/index.js`,
32 | format: 'esm',
33 | sourcemap: true,
34 | target: 'es2017',
35 | minify: true,
36 | });
37 | }
38 |
39 | await esbuild.build({
40 | plugins: [eik.plugin()],
41 | entryPoints: ['packages/index.ts'],
42 | bundle: true,
43 | outfile: `dist/eik/fabric-react-${version}.js`,
44 | format: 'esm',
45 | sourcemap: true,
46 | target: 'es2017',
47 | minify: true,
48 | });
49 |
--------------------------------------------------------------------------------
/packages/_helpers/affix.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { suffix, prefix } from '@fabric-ds/css/component-classes';
3 | import { classNames } from '@chbphone55/classnames';
4 |
5 | interface AffixProps {
6 | /** Defines a string value that labels the affix element. */
7 | 'aria-label'?: string;
8 |
9 | /** Affix added at the beginning of input */
10 | prefix?: boolean;
11 |
12 | /** Affix added at the end of input */
13 | suffix?: boolean;
14 |
15 | /** Displays a clear icon */
16 | clear?: boolean;
17 |
18 | /** Displays a search icon */
19 | search?: boolean;
20 |
21 | /** Displays a string */
22 | label?: string;
23 |
24 | /** Click handler paired with clear or search */
25 | onClick?: () => void;
26 | }
27 |
28 | export function Affix(props: AffixProps) {
29 | const classBase = props.prefix ? prefix : suffix;
30 |
31 | return React.createElement(
32 | props.label ? 'div' : 'button',
33 | {
34 | 'aria-label': !props.label ? props['aria-label'] : undefined,
35 | type: props.search ? 'submit' : props.clear ? 'reset' : undefined,
36 | onClick: props.onClick,
37 | className: classNames({
38 | [classBase.wrapper]: true,
39 | [classBase.wrapperWithLabel]: props.label,
40 | [classBase.wrapperWithIcon]: !props.label,
41 | }),
42 | },
43 | <>
44 | {props.clear && (
45 |
54 |
60 |
61 | )}
62 |
63 | {props.search && (
64 |
73 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | )}
89 |
90 | {props.label && {props.label} }
91 | >,
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/packages/_helpers/clickable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import { Item as ToggleItem } from '../toggle/src/item';
4 | import { useId } from '../utils/src/useId';
5 |
6 | export type ClickableProps = {
7 | /**
8 | * Passes radio type to the underlying toggle
9 | */
10 | radio?: boolean;
11 |
12 | /**
13 | * Passes checkbox type to the underlying toggle
14 | */
15 | checkbox?: boolean;
16 |
17 | /**
18 | * Value of the dead toggle
19 | */
20 | value?: string;
21 |
22 | /**
23 | * Clickable element children
24 | */
25 | children: JSX.Element | JSX.Element[] | string;
26 |
27 | /**
28 | * Redirect to url on click
29 | * If passed, clickable renders as an anchor tag allowing you to pass properties such as target, rel, etc.
30 | */
31 | href?: string;
32 |
33 | /**
34 | * Additional classnames to the toggle label
35 | */
36 | labelClassName?: string;
37 |
38 | /**
39 | * Click handler
40 | */
41 | onClick?: () => void;
42 | } & Partial> &
43 | Partial>;
44 |
45 | export function Clickable({
46 | children,
47 | radio,
48 | checkbox,
49 | value,
50 | ...props
51 | }: ClickableProps) {
52 | const id = useId();
53 | const type = radio ? 'radio' : 'checkbox';
54 |
55 | return radio || checkbox ? (
56 | undefined}
62 | value={value}
63 | name={`${props.name || id}:toggle`}
64 | >
65 | {children}
66 |
67 | ) : (
68 | React.createElement(
69 | props.href ? 'a' : 'button',
70 | {
71 | ...props,
72 | className: classNames(props.className, 'focus-ring'),
73 | type: props.href ? undefined : props.type || 'button',
74 | },
75 | <>
76 |
77 | {children}
78 | >,
79 | )
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/packages/_helpers/dead-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '@chbphone55/classnames';
2 | import React from 'react';
3 | import { Item } from '../toggle/src/item';
4 |
5 | export interface DeadToggleProps {
6 | /**
7 | * Passes radio type to the underlying toggle
8 | */
9 | radio?: boolean;
10 |
11 | /**
12 | * Passes checkbox type to the underlying toggle
13 | */
14 | checkbox?: boolean;
15 |
16 | /**
17 | * Value for the input
18 | */
19 | value?: string;
20 |
21 | /**
22 | * Whether the toggle is checked
23 | */
24 | checked?: boolean;
25 |
26 | /**
27 | * Additional classnames to the toggle wrapper
28 | */
29 | className?: string;
30 |
31 | /**
32 | * Additional classnames to the toggle label
33 | */
34 | labelClassName?: string;
35 | }
36 |
37 | export function DeadToggle(props: DeadToggleProps) {
38 | const type = props.radio ? 'radio' : 'checkbox';
39 |
40 | return (
41 |
47 | - undefined}
54 | value={props.value}
55 | checked={props.checked}
56 | />
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/packages/_helpers/expand-transition.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
2 | import { collapse, expand } from 'element-collapse';
3 |
4 | export function ExpandTransition({
5 | show,
6 | children,
7 | }: PropsWithChildren<{ show?: Boolean }>) {
8 | const [removeElement, setRemoveElement] = useState(!show);
9 | const expandableRef = useRef(null);
10 | const isMounted = useRef(false);
11 | const initialShow = useRef(show === true);
12 |
13 | function collapseElement(el: HTMLElement) {
14 | collapse(el, () => setRemoveElement(true));
15 | }
16 |
17 | function expandElement(el: HTMLElement) {
18 | expand(el);
19 | }
20 |
21 | if (show && removeElement) {
22 | setRemoveElement(false);
23 | }
24 |
25 | useEffect(() => {
26 | // Don't do anything at first render
27 | if (!isMounted.current) {
28 | isMounted.current = true;
29 | return;
30 | }
31 |
32 | if (!expandableRef.current) return;
33 |
34 | if (show) {
35 | expandElement(expandableRef.current);
36 | } else {
37 | collapseElement(expandableRef.current);
38 | }
39 | }, [show]);
40 |
41 | // Set initial style to prevent glitching bug
42 | const initialStyle = !initialShow.current ? 'overflow-hidden h-0' : undefined;
43 |
44 | return (
45 |
50 | {removeElement ? null : children}
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/packages/_helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { Clickable } from './clickable';
2 | export { DeadToggle } from './dead-toggle';
3 | export { Affix } from './affix';
4 | export { ExpandTransition } from './expand-transition';
5 | export { UnstyledHeading } from './unstyled-heading';
6 |
--------------------------------------------------------------------------------
/packages/_helpers/props.ts:
--------------------------------------------------------------------------------
1 | export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
2 |
--------------------------------------------------------------------------------
/packages/_helpers/unstyled-heading.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react';
2 | import { HeadingLevel } from './props';
3 |
4 | export const UnstyledHeading = ({
5 | level,
6 | children,
7 | ...attrs
8 | }: PropsWithChildren<{
9 | level?: HeadingLevel;
10 | }>) => {
11 | if (!level) {
12 | return {children} ;
13 | }
14 |
15 | // We must tell TypeScript that Heading is a valid HTML tag name
16 | const Heading = `h${level}` as keyof JSX.IntrinsicElements;
17 |
18 | return (
19 |
28 | {children}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/packages/alert/docs/Alert.mdx:
--------------------------------------------------------------------------------
1 | import { Alert } from '../src';
2 | import { Button } from '../../button/src';
3 |
4 | # Alert
5 |
6 | Alert is an inline component used for displaying different types of messages.
7 |
8 | ## Import
9 |
10 | ```js
11 | import { Alert } from '@fabric-ds/react';
12 | ```
13 |
14 | ## Visual Options
15 |
16 | ### Expandable behaviour
17 |
18 | ```jsx example
19 | function ExpandableAlert() {
20 | const [show, setShow] = React.useState(true);
21 |
22 | return (
23 | <>
24 | {
29 | setShow(false);
30 | setTimeout(() => setShow(true), 500);
31 | }}
32 | aria-controls="example-alert"
33 | aria-expanded={show}
34 | >
35 | Hide and show "info" variant of the alert
36 |
37 |
38 |
39 | This is "info" variant of the alert element
40 | With an additional description
41 | And a link to more information
42 |
43 | Primary button
44 |
45 | Secondary button
46 |
47 |
48 |
49 | >
50 | );
51 | }
52 | ```
53 |
54 | ### Negative
55 |
56 | ```jsx example
57 |
58 | This is "negative" variant of the alert element
59 |
60 | ```
61 |
62 | ### Positive
63 |
64 | ```jsx example
65 |
66 | This is "positive" variant of the alert element
67 |
68 | ```
69 |
70 | ### Warning
71 |
72 | ```jsx example
73 |
74 | This is "warning" variant of the alert element
75 |
76 | ```
77 |
78 | ### Info
79 |
80 | ```jsx example
81 |
82 | This is "info" variant of the alert element
83 |
84 | ```
85 |
86 | ## Accessibility
87 |
88 | Use the ARIA live region `role` attribute to provide meaning to the alert
89 | element (defaults to "alert"). If you want to remove the role from the alert and
90 | assign it to its particular child (e.g. title), you can do so by setting `role`
91 | property of the `Alert` component to an empty string and assigning a respective
92 | `role` attribute on the child element. Read more about live region `role`
93 | attribute on
94 | [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions#roles_with_implicit_live_region_attributes).
95 |
96 | ### Alert with "alert" role on a descendand element
97 |
98 | ```jsx example
99 | function ExpandableAlertWithOverridenRole() {
100 | const [show, setShow] = React.useState(true);
101 |
102 | return (
103 | <>
104 | {
109 | setShow(false);
110 | setTimeout(() => setShow(true), 500);
111 | }}
112 | aria-controls="example2-alert"
113 | aria-expanded={show}
114 | >
115 | Hide and show alert
116 |
117 |
118 |
119 | This is "info" variant of the alert element
120 |
121 | With an additional description
122 | And a link to more information
123 |
124 | Primary button
125 |
126 | Secondary button
127 |
128 |
129 |
130 | >
131 | );
132 | }
133 | ```
134 |
135 | ## Props
136 |
137 | ```props packages/alert/src/component.tsx
138 |
139 | ```
140 |
--------------------------------------------------------------------------------
/packages/alert/src/component.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '@chbphone55/classnames';
2 | import React, { PropsWithChildren, ReactElement } from 'react';
3 | import { AlertProps } from '.';
4 | import { ExpandTransition } from '../../_helpers';
5 |
6 | export function Alert({
7 | show,
8 | type,
9 | role = 'alert',
10 | children,
11 | ...props
12 | }: PropsWithChildren) {
13 | const { color, icon } = styleMap[type];
14 |
15 | return (
16 |
17 |
25 |
29 | {icon}
30 |
31 |
{children}
32 |
33 |
34 | );
35 | }
36 |
37 | const styleMap: {
38 | [key in AlertProps['type']]: { color: String; icon: ReactElement };
39 | } = {
40 | negative: {
41 | color: 'red',
42 | icon: (
43 |
49 |
53 |
59 |
60 |
61 | ),
62 | },
63 | positive: {
64 | color: 'green',
65 | icon: (
66 |
72 |
79 |
85 |
86 | ),
87 | },
88 | warning: {
89 | color: 'yellow',
90 | icon: (
91 |
97 |
101 |
107 |
108 |
109 | ),
110 | },
111 | info: {
112 | color: 'aqua',
113 | icon: (
114 |
120 |
121 |
127 |
128 | ),
129 | },
130 | };
131 |
--------------------------------------------------------------------------------
/packages/alert/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Alert } from './component';
2 | export type { AlertProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/alert/src/props.tsx:
--------------------------------------------------------------------------------
1 | export type AlertProps = {
2 | /**
3 | * Determines whether the alert should be visible
4 | */
5 | show?: Boolean;
6 | /**
7 | * Type of alert
8 | */
9 | type: 'negative' | 'positive' | 'warning' | 'info';
10 | /**
11 | * ARIA live region "role" attribute value
12 | */
13 | role?: string;
14 | /**
15 | * Additional classes to include
16 | */
17 | className?: string;
18 | /** Additional CSS styles for the container. */
19 | style?: React.CSSProperties;
20 | };
21 |
--------------------------------------------------------------------------------
/packages/alert/stories/Alert.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '../../button/src';
3 | import { Alert } from '../src';
4 |
5 | const metadata = { title: 'FeedbackIndicators/Alert' };
6 | export default metadata;
7 |
8 | export const Default = () => {
9 | const [show, setShow] = React.useState(true);
10 |
11 | return (
12 |
13 |
14 |
Negative
15 |
16 | This is a message that you've done something really wrong.
17 |
18 |
setShow(!show)}>
19 | {show ? 'Hide negative alert' : 'Show negative alert'}
20 |
21 |
22 |
23 |
Positive
24 |
25 | This is a message that gives you positive feedback.
26 |
27 |
28 |
29 |
Warning
30 |
31 | This is a message that shows a warning, might be nothing serious.
32 |
33 |
34 |
35 |
Info
36 |
37 | This is a message that enlightens you with some new cool information.
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const InteractiveContent = () => (
45 | <>
46 | This text attracts your attention right away
47 | This is the message text that can be short or a little bit long
48 | Link to more information
49 |
50 | Primary CTA
51 |
52 | Secondary
53 |
54 |
55 | >
56 | );
57 |
58 | export const WithInteractiveContent = () => {
59 | const [show, setShow] = React.useState(true);
60 |
61 | return (
62 |
63 |
64 |
Negative
65 |
66 |
67 |
68 |
setShow(!show)}>
69 | {show ? 'Hide negative alert' : 'Show negative alert'}
70 |
71 |
72 |
73 |
Positive
74 |
75 |
76 |
77 |
78 |
79 |
Warning
80 |
81 |
82 |
83 |
84 |
85 |
Info
86 |
87 |
88 |
89 |
90 |
91 | );
92 | };
93 |
--------------------------------------------------------------------------------
/packages/attention/docs/Attention.mdx:
--------------------------------------------------------------------------------
1 | import { Box } from '../../box/src';
2 | import { Button } from '../../button/src';
3 | import { Attention } from '../src';
4 |
5 | # Attention
6 |
7 | ## Import
8 |
9 | ```js
10 | import { Attention } from '@fabric-ds/react';
11 | ```
12 |
13 | ## Visual Options
14 |
15 | ### Callout
16 |
17 | ```jsx example
18 |
19 |
20 | I am a box full of info
21 |
22 |
23 | I'm a callout because that box over there is new or something
24 |
25 |
26 | ```
27 |
28 | ### Tooltip
29 |
30 | ```jsx example
31 | function Example() {
32 | const [show, setShow] = React.useState(false);
33 | const targetEl = React.useRef();
34 |
35 | return (
36 |
37 |
setShow(true)}
41 | onMouseLeave={() => setShow(false)}
42 | >
43 | hover this for useless info
44 |
45 |
51 | lol i am a popover
52 |
53 |
54 | );
55 | }
56 | ```
57 |
58 | ### Popover
59 |
60 | ```jsx example
61 | function Example() {
62 | const [show, setShow] = React.useState(false);
63 | const containerRef = React.useRef();
64 | const targetEl = React.useRef();
65 |
66 | React.useEffect(() => {
67 | function onBlurHandler(e) {
68 | if (containerRef.current && !containerRef.current.contains(e.target)) {
69 | setShow(false);
70 | }
71 | }
72 | document.addEventListener('mousedown', onBlurHandler);
73 | return () => {
74 | document.removeEventListener('mousedown', onBlurHandler);
75 | };
76 | });
77 |
78 | return (
79 |
80 |
setShow(!show)}
84 | className="w-max mb-0"
85 | ref={targetEl}
86 | >
87 | Open menu
88 |
89 |
95 |
96 |
100 | Hello
101 |
102 |
106 | World
107 |
108 |
109 |
110 |
111 | );
112 | }
113 | ```
114 |
115 | ## Attention Props
116 |
117 | ```props packages/attention/src/component.tsx
118 |
119 | ```
120 |
--------------------------------------------------------------------------------
/packages/attention/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useEffect, useRef, useState } from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import {
4 | opposites,
5 | rotation,
6 | useRecompute as recompute,
7 | arrowLabels,
8 | } from '@fabric-ds/core/attention';
9 | import { attention as c } from '@fabric-ds/css/component-classes';
10 | import { ArrowProps, AttentionProps } from './props';
11 |
12 | export function Attention(props: AttentionProps) {
13 | const {
14 | noArrow,
15 | isShowing,
16 | children,
17 | placement,
18 | targetEl,
19 | className,
20 | ...rest
21 | } = props;
22 |
23 | const [actualDirection, setActualDirection] = useState(placement);
24 | // Don't show attention element before its position is computed on first render
25 | const [isVisible, setIsVisible] = useState(false);
26 |
27 | const isMounted = useRef(true);
28 | const attentionRef = useRef(null);
29 | const arrowRef = useRef(null);
30 |
31 | const attentionState = {
32 | get isShowing() {
33 | return isShowing;
34 | },
35 | get isCallout() {
36 | return rest.callout;
37 | },
38 | get actualDirection() {
39 | return actualDirection;
40 | },
41 | set actualDirection(v) {
42 | setActualDirection(v);
43 | },
44 | get directionName() {
45 | return placement;
46 | },
47 | get arrowEl() {
48 | return arrowRef.current;
49 | },
50 | get attentionEl() {
51 | return attentionRef.current;
52 | },
53 | set attentionEl(v) {
54 | attentionRef.current = v;
55 | },
56 | get targetEl() {
57 | return targetEl?.current;
58 | },
59 | get noArrow() {
60 | return props.noArrow;
61 | },
62 | };
63 |
64 | // Recompute on re-render
65 | useEffect(() => {
66 | recompute(attentionState);
67 | });
68 |
69 | useEffect(() => {
70 | if (isMounted.current) {
71 | isMounted.current = false;
72 |
73 | // update attention's visibility after first render if showing by default or it's of type callout
74 | if (isShowing === true || props.callout) {
75 | setIsVisible(isShowing);
76 | }
77 | } else {
78 | setIsVisible(isShowing);
79 | }
80 | }, [isShowing, props.callout]);
81 |
82 | return (
83 |
94 |
102 | {!props.noArrow && (
103 |
104 | )}
105 |
{props.children}
106 |
107 |
108 | );
109 | }
110 |
111 | const Arrow = forwardRef((props, ref) => {
112 | const { direction, tooltip, callout, popover } = props;
113 |
114 | const arrowDirection = opposites[direction];
115 |
116 | return (
117 |
138 | );
139 | });
140 |
--------------------------------------------------------------------------------
/packages/attention/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Attention } from './component';
2 |
--------------------------------------------------------------------------------
/packages/attention/src/props.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject } from 'react';
2 |
3 | export type Directions = 'top' | 'right' | 'bottom' | 'left';
4 |
5 | export type ArrowProps = {
6 | /**
7 | * Opposite direction of which the arrow should point
8 | */
9 | direction: Directions;
10 |
11 | /**
12 | * Render tooltip
13 | */
14 | tooltip?: boolean;
15 |
16 | /**
17 | * Render callout
18 | */
19 | callout?: boolean;
20 |
21 | /**
22 | * Render popover
23 | */
24 | popover?: boolean;
25 |
26 | /**
27 | * Forward arrow ref so Attention element can use it
28 | */
29 | ref?: React.Ref;
30 | };
31 |
32 | export type AttentionProps = {
33 | /**
34 | * Render Attention element without arrow
35 | */
36 | noArrow?: Boolean;
37 |
38 | /**
39 | * Whether Attention element is shown
40 | * Used for tooltip
41 | */
42 | isShowing?: boolean;
43 |
44 | /**
45 | * Elements inside of the Attention component
46 | */
47 | children?: JSX.Element[] | JSX.Element;
48 |
49 | /**
50 | * Placement according to the target element
51 | * Arrow would be on the opposite side of this position
52 | */
53 | placement: Directions;
54 |
55 | /**
56 | * Container the Attention component is rendered relatively to
57 | */
58 | targetEl?: MutableRefObject;
59 |
60 | /**
61 | * Extend the Attention component container styling
62 | */
63 | className?: string;
64 | } & Omit;
65 |
--------------------------------------------------------------------------------
/packages/attention/stories/Attention.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Attention } from '../src';
3 | import { Box } from '../../box/src';
4 | import { Button } from '../../button/src';
5 |
6 | const metadata = { title: 'Overlays/Attention' };
7 | export default metadata;
8 |
9 | export function Callout() {
10 | return (
11 |
12 |
13 | I am a box full of info
14 |
15 |
16 |
17 | I'm a callout because that box over there is new or something
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export function Tooltip() {
25 | const [show, setShow] = React.useState(false);
26 | const targetEl = React.useRef();
27 |
28 | return (
29 |
30 |
setShow(true)}
34 | onMouseLeave={() => setShow(false)}
35 | >
36 | hover this for useless info
37 |
38 |
44 | lol i am a popover
45 |
46 |
47 | );
48 | }
49 |
50 | export function Popover() {
51 | const [show, setShow] = React.useState(false);
52 | const containerRef = React.useRef();
53 | const targetEl = React.useRef();
54 |
55 | React.useEffect(() => {
56 | function onBlurHandler(e) {
57 | if (containerRef.current && !containerRef.current.contains(e.target)) {
58 | setShow(false);
59 | }
60 | }
61 | document.addEventListener('mousedown', onBlurHandler);
62 | return () => {
63 | document.removeEventListener('mousedown', onBlurHandler);
64 | };
65 | });
66 |
67 | return (
68 |
69 |
setShow(!show)}
73 | className="w-max mb-0"
74 | ref={targetEl}
75 | >
76 | Open menu
77 |
78 |
84 |
85 |
89 | Hello
90 |
91 |
95 | World
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/packages/box/docs/Box.mdx:
--------------------------------------------------------------------------------
1 | import { Box } from '../src';
2 | import { Clickable } from '../../_helpers';
3 |
4 | # Box
5 |
6 | Box is a layout component used for separating content areas on a page.
7 |
8 | ## Import
9 |
10 | ```js
11 | import { Box } from '@fabric-ds/react';
12 | ```
13 |
14 | ## Visual Options
15 |
16 | ### Default
17 |
18 | ```jsx example
19 |
20 | Default example
21 | Box contents go here.
22 |
23 | ```
24 |
25 | ### Info
26 |
27 | ```jsx example
28 |
29 | Info example
30 | Box contents go here.
31 |
32 | ```
33 |
34 | ### Bordered
35 |
36 | ```jsx example
37 |
38 | Bordered example
39 | Box contents go here.
40 |
41 | ```
42 |
43 | ### Clickable (using button)
44 |
45 | ```jsx example
46 |
47 |
48 | alert('hey')}>
49 | Clickable example
50 |
51 |
52 | Box contents go here.
53 |
54 | ```
55 |
56 | ### Clickable (using anchor)
57 |
58 | ```jsx example
59 |
60 |
61 |
66 | Clickable example
67 |
68 |
69 | Box contents go here.
70 |
71 | ```
72 |
73 | ### Neutral
74 |
75 | ```jsx example
76 |
77 | Neutral example
78 | Box contents go here.
79 |
80 | ```
81 |
82 | ## Props
83 |
84 | ```props packages/box/src/component.tsx
85 |
86 | ```
87 |
88 | ## Clickable props
89 |
90 | ```props packages/_helpers/clickable.tsx
91 |
92 | ```
93 |
--------------------------------------------------------------------------------
/packages/box/src/component.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '@chbphone55/classnames';
2 | import { box } from '@fabric-ds/css/component-classes';
3 | import React from 'react';
4 | import { BoxProps } from './props';
5 |
6 | export function Box(props: BoxProps) {
7 | const {
8 | children,
9 | as = 'div',
10 | bleed,
11 | clickable,
12 | neutral,
13 | bordered,
14 | info,
15 | ...rest
16 | } = props;
17 |
18 | return React.createElement(
19 | as,
20 | {
21 | ...(rest as Omit as {}),
22 | className: classNames(
23 | box.box,
24 | {
25 | [box.bleed]: bleed,
26 | [box.clickable]: clickable,
27 | 'bg-aqua-50': info,
28 | 'hover:bg-aqua-100 active:bg-aqua-200': info && clickable,
29 | 'bg-bluegray-100': neutral,
30 | 'hover:bg-bluegray-200 active:bg-bluegray-300': neutral && clickable,
31 | 'border-2 border-bluegray-300': bordered,
32 | },
33 | props.className,
34 | ),
35 | },
36 | children,
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/packages/box/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Box } from './component';
2 | export type { BoxProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/box/src/props.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export interface BoxProps {
3 | /**
4 | * Expand element children
5 | */
6 | children: JSX.Element | JSX.Element[];
7 |
8 | /**
9 | * Additional classes to include
10 | */
11 | className?: string;
12 |
13 | /**
14 | * CSS styles to inline on the component
15 | */
16 | style?: React.CSSProperties;
17 |
18 | /**
19 | * Action to be called when the component is clicked
20 | */
21 | onClick?: (
22 | e: React.MouseEvent | React.KeyboardEvent,
23 | ) => void;
24 |
25 | /**
26 | * Allows customization of the underlying HTML element
27 | * @default div
28 | */
29 | as?: string;
30 |
31 | /**
32 | * Toggles bleed, makes a box full-width on mobile
33 | * @default false
34 | */
35 | bleed?: boolean;
36 |
37 | /**
38 | * Applies focus and pointer helpers, should be used with other styling to indicate clickability
39 | * @default false
40 | */
41 | clickable?: boolean;
42 |
43 | /**
44 | * @default false
45 | */
46 | bordered?: boolean;
47 |
48 | /**
49 | * Styles the box with light blue color
50 | * @default false
51 | */
52 | info?: boolean;
53 |
54 | /**
55 | * Style the box with light gray color
56 | * @default false
57 | */
58 | neutral?: boolean;
59 | }
60 |
--------------------------------------------------------------------------------
/packages/box/stories/Box.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '../src';
3 | import { Clickable } from '../../_helpers';
4 |
5 | const metadata = { title: 'Layout/Box' };
6 | export default metadata;
7 |
8 | export const Default = () => (
9 |
10 | I have default styles
11 |
12 | );
13 |
14 | export const Info = () => (
15 |
16 | I have info styles
17 |
18 | );
19 |
20 | export const Bleed = () => (
21 |
22 | I am full width on mobile
23 |
24 | );
25 |
26 | export const Bordered = () => (
27 |
28 | I am bordered with no fill
29 |
30 | );
31 |
32 | export const Neutral = () => (
33 |
34 | I am a neutral colour
35 |
36 | );
37 |
38 | export const ClickableButton = () => (
39 |
40 |
41 | alert('hey')}>
42 | Clickable example
43 |
44 |
45 | Other contents will go here.
46 |
47 | );
48 |
49 | export const ClickableAnchor = () => (
50 |
51 |
52 |
57 | Clickable example
58 |
59 |
60 | Other contents will go here.
61 |
62 | );
63 |
64 | export const As = () => (
65 |
66 | I'm wrapped in a section tag
67 |
68 | );
69 |
--------------------------------------------------------------------------------
/packages/breadcrumbs/docs/Breadcrumbs.mdx:
--------------------------------------------------------------------------------
1 | import { Breadcrumbs } from '../src';
2 |
3 | # Breadcrumbs
4 |
5 | Breadcrumbs show the navigation structure for the current location.
6 |
7 | ## Import
8 |
9 | ```js
10 | import { Breadcrumbs } from '@fabric-ds/react';
11 | ```
12 |
13 | ## Example
14 |
15 | ```jsx example
16 |
17 | Eiendom
18 | Bolig til salgs
19 |
20 | Oslo
21 |
22 |
23 | ```
24 |
25 | ## Content
26 |
27 | Breadcrumbs expect their component children to be the link "crumbs" that make up
28 | the navigation structure. The component will interject a separator between the
29 | crumbs.
30 |
31 | ## Accessibility
32 |
33 | Breadcrumbs should have a label that identifies the structure as a breadcrumb
34 | trail to screen readers. By default, `aria-label` is set to "Her er
35 | du" .
36 |
37 | It is recommended that the crumb for the current page has the
38 | `aria-current="page"` attribute set. Usually this is the last crumb in the
39 | trail.
40 |
41 | ## Props
42 |
43 | ```props packages/breadcrumbs/src/component.tsx
44 |
45 | ```
46 |
--------------------------------------------------------------------------------
/packages/breadcrumbs/src/component.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import type { BreadcrumbsProps } from './props';
4 | import { interleave } from '@fabric-ds/core/breadcrumbs';
5 |
6 | export const Breadcrumbs = (props: BreadcrumbsProps) => {
7 | const { children, className, ...rest } = props;
8 | const ariaLabel = props['aria-label'] || 'Her er du';
9 |
10 | // Handles arrays of nodes passed as children
11 | const flattenedChildren = children.flat(Infinity);
12 |
13 | return (
14 |
19 | {ariaLabel}
20 | {interleave(
21 | flattenedChildren,
22 |
23 | /
24 | ,
25 | ).map((element, index) => (
26 | {element}
27 | ))}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/packages/breadcrumbs/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Breadcrumbs } from './component';
2 | export type { BreadcrumbsProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/breadcrumbs/src/props.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type BreadcrumbsProps = {
4 | // prop table doc seems unable to pull out default value with the rename in the function definition
5 | /**
6 | * Defines a string value that labels the current element.
7 | * @default Her er du
8 | */
9 | 'aria-label'?: string;
10 |
11 | className?: string;
12 |
13 | children: React.ReactNode[];
14 |
15 | style?: React.CSSProperties;
16 | // omit aria-label for better prop table docs
17 | } & Omit<
18 | React.PropsWithoutRef,
19 | 'aria-label' | 'children'
20 | >;
21 |
--------------------------------------------------------------------------------
/packages/breadcrumbs/stories/Breadcrumbs.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Breadcrumbs } from '../src';
3 |
4 | const metadata = { title: 'Navigation/Breadcrumbs' };
5 | export default metadata;
6 |
7 | export const BasicExample = () => (
8 |
9 | Item 1
10 | Item 2
11 | Item 3
12 |
13 | );
14 |
15 | export const ExampleWithArray = () => {
16 | const breadcrumbs = [
17 | { id: 1, name: 'Item 1' },
18 | { id: 2, name: 'Item 2' },
19 | { id: 3, name: 'Item 3' },
20 | { id: 4, name: 'Item 4' },
21 | ];
22 |
23 | return (
24 |
25 | {breadcrumbs.slice(0, -1).map((collection) => (
26 |
27 | {collection.name}
28 |
29 | ))}
30 | {breadcrumbs.at(-1)!.name}
31 |
32 | );
33 | };
34 |
35 | type BreadcrumbsLink = { id: number; name: string };
36 |
37 | export const ExampleWithNestedArrays = () => {
38 | const breadcrumbs = [
39 | { id: 1, name: 'Item 1' },
40 | { id: 2, name: 'Item 2' },
41 | { id: 3, name: 'Item 3' },
42 | { id: 4, name: 'Item 4' },
43 | [
44 | { id: 5, name: 'Item 5' },
45 | { id: 6, name: 'Item 6' },
46 | [
47 | { id: 7, name: 'Item 7' },
48 | { id: 8, name: 'Item 8' },
49 | ],
50 | ],
51 | { id: 0, name: 'Item 9' },
52 | ];
53 |
54 | const lastItem = breadcrumbs.at(-1) as BreadcrumbsLink;
55 |
56 | return (
57 |
58 | {breadcrumbs
59 | .slice(0, -1)
60 | .map(
61 | (
62 | collection:
63 | | BreadcrumbsLink
64 | | Array,
65 | ) => {
66 | if ('name' in collection) {
67 | return (
68 |
69 | {collection.name}
70 |
71 | );
72 | }
73 |
74 | return collection.map((coll) => {
75 | if ('name' in coll) {
76 | return (
77 |
78 | {coll.name}
79 |
80 | );
81 | }
82 |
83 | return coll.map((c) => (
84 |
85 | {c.name}
86 |
87 | ));
88 | });
89 | },
90 | )}
91 | {lastItem.name}
92 |
93 | );
94 | };
95 |
--------------------------------------------------------------------------------
/packages/button/docs/Button.mdx:
--------------------------------------------------------------------------------
1 | import { Button } from '../src';
2 |
3 | # Button
4 |
5 | Buttons are used to perform actions, with different visuals for different needs.
6 |
7 | ## Import
8 |
9 | ```js
10 | import { Button } from '@fabric-ds/react';
11 | ```
12 |
13 | ## Example
14 |
15 | ```jsx example
16 | Save
17 | ```
18 |
19 | ## Migrating from Troika
20 |
21 | - You should no longer include or import any Troika button CSS.
22 | - The variants are no longer different named exports. Use component properties
23 | instead.
24 | - There is now a single button component for all cases. As a quick guide:
25 | - `` should be migrated to ``.
26 | - `` should be migrated to ``.
27 | - `` should be migrated to ``.
28 | - `` should be migrated to ``.
29 | - `` should be migrated to ``.
30 | - The `inProgress` property should be changed to `loading`.
31 | - The `disabled` has been removed as it is an anti-pattern (see below).
32 |
33 | ## Accessibility
34 |
35 | If the button doesn't have visible text content, such as when used with only an
36 | icon, an `aria-label` prop must be provided for accessibility.
37 |
38 | ## Visual options
39 |
40 | ### Primary
41 |
42 | The primary button is a call to action. As a general rule, there should only be
43 | one of them on the screen. This guides the user towards the happy path.
44 |
45 | ```jsx example
46 | Save
47 | ```
48 |
49 | ### Negative
50 |
51 | Used for destructive actions, like deletion. Shouldn't be used on the same
52 | screen as a primary button.
53 |
54 | ```jsx example
55 | Delete
56 | ```
57 |
58 | ### Secondary
59 |
60 | Secondary buttons are without an outline, and are often used for secondary or
61 | tertiary actions. This is the default so you may simply omit the secondary
62 | property
63 |
64 | ```jsx example
65 |
66 | Save
67 | Save
68 |
69 | ```
70 |
71 | ### Loading/In progress
72 |
73 | Used for visual feedback that the action the user triggered is loading.
74 |
75 | ```jsx example
76 | Save
77 | ```
78 |
79 | ### Small
80 |
81 | ```jsx example
82 | Small
83 | ```
84 |
85 | ### Pill
86 |
87 | ```jsx example
88 | Pill
89 | ```
90 |
91 | ### Link
92 |
93 | Buttons will be rendered as an anchor (a tag) if they use an `href` attribute.
94 |
95 | ```jsx example
96 | Link
97 | ```
98 |
99 | But if you need a button to look like a link, use the `link` property.
100 |
101 | ```jsx example
102 | Link
103 | ```
104 |
105 | ### Disabled
106 |
107 | Disabled is an anti-pattern and is not supported. There will ALWAYS be users who
108 | don't understand why an element is disabled, or users who can't even see that it
109 | is disabled because of poor lighting conditions or other reasons.
110 |
111 | ## Props
112 |
113 | ```props packages/button/src/component.tsx
114 |
115 | ```
116 |
--------------------------------------------------------------------------------
/packages/button/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, Ref } from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import type { ButtonProps } from './props';
4 |
5 | export const Button = forwardRef<
6 | HTMLButtonElement | HTMLAnchorElement,
7 | ButtonProps
8 | >((props, ref) => {
9 | const {
10 | primary,
11 | secondary,
12 | negative,
13 | utility,
14 | quiet,
15 | small,
16 | link,
17 | pill,
18 | loading,
19 | ...rest
20 | } = props;
21 |
22 | const classes = classNames(props.className, {
23 | button: true,
24 | // primary buttons
25 | 'button--primary': primary,
26 | 'button--destructive': negative,
27 | // quiet
28 | 'button--flat': secondary && quiet,
29 | 'button--destructive-flat': negative && quiet,
30 | 'button--utility-flat': utility && quiet,
31 | // others
32 | 'button--small': small,
33 | 'button--utility': utility && !quiet,
34 | 'button--link': link,
35 | 'button--pill': pill,
36 | 'button--in-progress': loading,
37 | });
38 |
39 | return (
40 | <>
41 | {props.href ? (
42 | }
47 | className={classes}
48 | >
49 | {props.children}
50 |
51 | ) : (
52 | }
56 | className={classes}
57 | >
58 | {props.children}
59 |
60 | )}
61 | {props.loading ? (
62 |
68 | ) : null}
69 | >
70 | );
71 | });
72 |
--------------------------------------------------------------------------------
/packages/button/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from './component';
2 | export type { ButtonProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/button/src/props.tsx:
--------------------------------------------------------------------------------
1 | export type ButtonProps = {
2 | children: React.ReactNode;
3 |
4 | /**
5 | * Additional classes to include
6 | */
7 | className?: string;
8 |
9 | /**
10 | * Action to be called when the component is clicked
11 | */
12 | onClick?: (e: React.MouseEvent) => void;
13 |
14 | /**
15 | * CSS styles to inline on the component
16 | */
17 | style?: React.CSSProperties;
18 |
19 | /**
20 | * Button type, only applied when href is not set.
21 | * @default button
22 | */
23 | type?: 'button' | 'submit' | 'reset';
24 |
25 | /**
26 | * Set the button to be a primary, call to action button. Can be combined with `small`.
27 | * @default false
28 | */
29 | primary?: boolean;
30 |
31 | /**
32 | * Set the button to be a secondary, flat style button. Can be combined with `quiet` and `small`.
33 | * @default true
34 | */
35 | secondary?: boolean;
36 |
37 | /**
38 | * Set the button to be a negative, destructive style button. Can be combined with `quiet` and `small`.
39 | * @default false
40 | */
41 | negative?: boolean;
42 |
43 | /**
44 | * Set the button to be a utility style button. Can be combined with `small`.
45 | * @default false
46 | */
47 | utility?: boolean;
48 |
49 | /**
50 | * Quieten down the button, can be combined with other button types
51 | * @default false
52 | */
53 | quiet?: boolean;
54 |
55 | /**
56 | * Set the button to be a small size, can be combined with other button types
57 | * @default false
58 | */
59 | small?: boolean;
60 |
61 | /**
62 | * Set the button to look like a link. Can be combined with `small`.
63 | * @default false
64 | */
65 | link?: boolean;
66 |
67 | /**
68 | * Set the button to look like a pill style button
69 | * @default false
70 | */
71 | pill?: boolean;
72 |
73 | /**
74 | * Set the button to look like it is in progress, can be combined with other button types. Can be combined with any button type.
75 | * @default false
76 | */
77 | loading?: boolean;
78 |
79 | /**
80 | * Set the href for the location where clicking the button will take you to. Uses an a tag instead of a button tag for the underlying implementation
81 | */
82 | href?: string;
83 |
84 | /**
85 | * Anchor target, see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
86 | */
87 | target?: string;
88 |
89 | /**
90 | * The relationship of the linked URL
91 | */
92 | rel?: string;
93 | } & Omit<
94 | React.PropsWithoutRef,
95 | // omit children here, because we don't want children to be optional
96 | 'children' | 'onClick'
97 | >;
98 |
--------------------------------------------------------------------------------
/packages/card/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { card as c } from '@fabric-ds/css/component-classes';
3 | import { classNames } from '@chbphone55/classnames';
4 | import { CardProps } from './props';
5 | import { useLogDeprecationWarning } from '../../utils/src';
6 |
7 | export function Card(props: CardProps) {
8 | const { as = 'div', children, flat, ...rest } = props;
9 |
10 | useLogDeprecationWarning({
11 | condition: !!props.onClick,
12 | message:
13 | "'onClick' prop in Card is deprecated. Use Clickable component to handle click events in Cards.",
14 | });
15 |
16 | return React.createElement(
17 | as,
18 | {
19 | ...rest,
20 | className: classNames(props.className, {
21 | [c.card]: true,
22 | [c.cardShadow]: !props.flat,
23 | [c.cardSelected]: props.selected,
24 | [c.cardFlat]: props.flat,
25 | [props.selected ? c.cardFlatSelected : c.cardFlatUnselected]:
26 | props.flat,
27 | }),
28 | // @balbinak(08.11.22): onClick support in Card is deprecated. Remove when Fabric React users are ready for this major change
29 | tabIndex: props.onClick ? 0 : undefined,
30 | onKeyDown: props.onClick
31 | ? (e) => {
32 | if (
33 | typeof props.onClick === 'function' &&
34 | (e.key === 'Enter' || e.key === ' ')
35 | ) {
36 | e.preventDefault();
37 | props.onClick();
38 | return;
39 | }
40 | }
41 | : undefined,
42 | },
43 | <>
44 | {props.onClick && (
45 |
51 | Velg
52 |
53 | )}
54 |
55 | {!props.flat && (
56 |
63 | )}
64 |
65 | {children}
66 | >,
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/packages/card/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Card } from './component';
2 | export type { CardProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/card/src/props.tsx:
--------------------------------------------------------------------------------
1 | export interface CardProps {
2 | /**
3 | * Removes box shadow around card
4 | */
5 | flat?: boolean;
6 |
7 | /**
8 | * The contents of the Card
9 | */
10 | children: JSX.Element | JSX.Element[];
11 |
12 | /**
13 | * If the card is selected
14 | */
15 | selected?: boolean;
16 |
17 | /**
18 | * The wrapping container element
19 | * @default div
20 | */
21 | as?: string;
22 |
23 | /**
24 | * Add your own custom styles to the container element
25 | */
26 | className?: string;
27 |
28 | /**
29 | * When the card is clicked (deprecated)
30 | */
31 | onClick?: () => void;
32 | }
33 |
--------------------------------------------------------------------------------
/packages/combobox/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { ComboboxProps } from './props';
2 | export { Combobox } from './component';
3 |
--------------------------------------------------------------------------------
/packages/combobox/src/props.ts:
--------------------------------------------------------------------------------
1 | export type ComboboxOption = {
2 | value: string;
3 | label?: string;
4 | };
5 |
6 | export type OptionWithIdAndMatch = ComboboxOption & {
7 | id: string;
8 | currentInputValue: string;
9 | };
10 |
11 | export type ComboboxProps = {
12 | /**
13 | * Unique identifier for the input field
14 | */
15 | id?: string;
16 |
17 | /**
18 | * The available options to select from
19 | */
20 | options: ComboboxOption[];
21 |
22 | /**
23 | * Label above input
24 | */
25 | label?: string;
26 |
27 | /**
28 | * Input placeholder
29 | */
30 | placeholder?: string;
31 |
32 | /**
33 | * The TextField input value
34 | */
35 | value: string;
36 |
37 | /**
38 | * Whether the popover opens when focus is on the text field.
39 | * @default false
40 | */
41 | openOnFocus?: boolean;
42 |
43 | /**
44 | * Select active option on blur
45 | * @default true
46 | */
47 | selectOnBlur?: boolean;
48 |
49 | /**
50 | * Whether the matching text segments in the options should be highlighted. Customise the styling by using CSS selectors to override `[data-combobox-text-match]`.
51 | * This uses the default matching algorithm. Use the `highlightValueMatch` to pass your own matching function.
52 | * @default false
53 | */
54 | matchTextSegments?: boolean;
55 |
56 | /** Disable client-side static filtering
57 | * @default false
58 | */
59 | disableStaticFiltering?: boolean;
60 |
61 | /**
62 | * Pass your own function for highlight matching
63 | */
64 | highlightValueMatch?: (
65 | optionValue: string,
66 | inputValue: string,
67 | ) => React.ReactNode;
68 |
69 | /**
70 | * Called when the user selects an option
71 | */
72 | onSelect?(value: string): void;
73 |
74 | /**
75 | * Called when the value of the input changes
76 | */
77 | onChange(value: string): void;
78 |
79 | /**
80 | * Called when the input is focus
81 | */
82 | onFocus?: () => void;
83 |
84 | /**
85 | * Called when the input loses focus with the current navigation value or input value
86 | */
87 | onBlur?: (value: string) => void;
88 |
89 | /** Renders the input field in an invalid state. Often paired together with `helpText` to provide feedback about the error. */
90 | invalid?: boolean;
91 |
92 | /** The content to display as the help text. */
93 | helpText?: React.ReactNode;
94 |
95 | /**
96 | * Additional container styling
97 | */
98 | className?: string;
99 |
100 | /**
101 | * Additional list styling
102 | */
103 | listClassName?: string;
104 |
105 | /**
106 | * Defines a string value that labels the current element. Must be set if `aria-labelledby` is not defined,
107 | */
108 | 'aria-label'?: string;
109 |
110 | /**
111 | * Identifies the element (or elements) that labels the current element. Must be set if `aria-label` is not defined.
112 | */
113 | 'aria-labelledby'?: string;
114 |
115 | /** For affix use */
116 | children?: React.ReactNode;
117 |
118 | /** Whether to show optional text */
119 | optional?: boolean;
120 | } & Omit<
121 | React.PropsWithoutRef,
122 | 'onChange' | 'type' | 'value' | 'label'
123 | >;
124 |
--------------------------------------------------------------------------------
/packages/combobox/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from '../../utils/src/useId';
2 | import { ComboboxOption, OptionWithIdAndMatch } from './props';
3 |
4 | // Add id and match to the object
5 | export function createOptionsWithIdAndMatch(
6 | options: ComboboxOption[],
7 | currentInputValue: string,
8 | ): OptionWithIdAndMatch[] {
9 | return options.map((option) => ({
10 | ...option,
11 | id: generateId(),
12 | currentInputValue,
13 | }));
14 | }
15 |
16 | function isPlural(array) {
17 | return array.length > 1 || array.length === 0;
18 | }
19 |
20 | export function getAriaText(options: OptionWithIdAndMatch[], value: string) {
21 | if (!options) return;
22 |
23 | const filteredOptionsByInputValue = options.filter((option) =>
24 | option.value.toLowerCase().includes(value.toLowerCase()),
25 | );
26 |
27 | return filteredOptionsByInputValue.length
28 | ? `${filteredOptionsByInputValue.length} resultat${
29 | isPlural(filteredOptionsByInputValue) ? 'er' : ''
30 | }`
31 | : `Ingen resultater`;
32 | }
33 |
--------------------------------------------------------------------------------
/packages/expandable/docs/Expandable.mdx:
--------------------------------------------------------------------------------
1 | import { IconBag16 } from '@fabric-ds/icons/react';
2 | import { Expandable } from '../src';
3 |
4 | # Expandable
5 |
6 | Expandable is a layout component used for creating expandable content areas on a
7 | page.
8 |
9 | ## Import
10 |
11 | ```js
12 | import { Expandable } from '@fabric-ds/react';
13 | ```
14 |
15 | ## Visual Options
16 |
17 | ### Default
18 |
19 | ```jsx example
20 |
21 | Expandable contents go here.
22 |
23 | ```
24 |
25 | ### Expandable box
26 |
27 | ```jsx example
28 |
29 | Expandable contents go here.
30 |
31 | ```
32 |
33 | ### Expandable info box
34 |
35 | ```jsx example
36 |
37 | Expandable contents go here.
38 |
39 | ```
40 |
41 | ### Expandable info box with custom title
42 |
43 | ```jsx example
44 |
47 |
48 | This is a title with an icon
49 |
50 | }
51 | box
52 | info
53 | >
54 | Expandable contents go here.
55 |
56 | ```
57 |
58 | ### Expandable animated box
59 |
60 | ```jsx example
61 |
62 | Expandable contents go here.
63 |
64 | ```
65 |
66 | ### Expandable with an h2 wrapping the toggle button
67 |
68 | ```jsx example
69 |
70 | Expandable contents go here.
71 |
72 | ```
73 |
74 | ### onChange event
75 |
76 | ```jsx example
77 | console.log(state)}>
78 | onChange example
79 | Expandable contents go here.
80 |
81 | ```
82 |
83 | ### The expanded prop
84 |
85 | You can set whether the component is open or collapsed using the `expanded`
86 | prop.
87 |
88 | ```jsx example
89 |
90 | ...expanded
91 |
92 | ```
93 |
94 | ### Controlling the component
95 |
96 | If you need to take control of expansion, use the `onChange` event in
97 | combination with the `expanded` prop
98 |
99 | ```jsx example
100 | function Example() {
101 | const [open, setOpen] = React.useState(true);
102 | return (
103 |
110 | I am expanded
111 |
112 | );
113 | }
114 | ```
115 |
116 | ## Props
117 |
118 | ```props packages/expandable/src/component.tsx
119 |
120 | ```
121 |
--------------------------------------------------------------------------------
/packages/expandable/src/component.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '@chbphone55/classnames';
2 | import {
3 | box as boxClasses,
4 | buttonReset,
5 | } from '@fabric-ds/css/component-classes';
6 | import React from 'react';
7 | import { ExpandTransition, UnstyledHeading } from '../../_helpers';
8 | import { ExpandableProps } from './props';
9 |
10 | export function Expandable(props: ExpandableProps) {
11 | const {
12 | children,
13 | expanded = false,
14 | title = '',
15 | info = false,
16 | box = false,
17 | bleed = false,
18 | buttonClass = '',
19 | contentClass = '',
20 | className,
21 | onChange,
22 | chevron = true,
23 | animated,
24 | headingLevel,
25 | ...rest
26 | } = props;
27 |
28 | const [stateExpanded, setStateExpanded] = React.useState(expanded);
29 |
30 | React.useEffect(() => {
31 | setStateExpanded(expanded);
32 | }, [expanded]);
33 |
34 | const toggleExpandable = (state) => {
35 | setStateExpanded(!state);
36 | if (onChange) onChange(!state);
37 | };
38 |
39 | return (
40 |
48 |
49 | toggleExpandable(stateExpanded)}
59 | >
60 |
61 | {typeof title === 'string' ? (
62 |
{title}
63 | ) : (
64 | title
65 | )}
66 | {chevron && (
67 |
91 | )}
92 |
93 |
94 |
95 |
96 |
102 | {children}
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | function ExpansionBehaviour({ animated, stateExpanded, children }) {
110 | return animated ? (
111 | {children}
112 | ) : (
113 |
120 | {children}
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/packages/expandable/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Expandable } from './component';
2 | export type { ExpandableProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/expandable/src/props.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { HeadingLevel } from '../../_helpers/props';
3 |
4 | export type ExpandableProps = {
5 | children: React.ReactNode;
6 |
7 | /**
8 | * Additional classes to include
9 | */
10 | className?: string;
11 |
12 | /**
13 | * CSS styles to inline on the component
14 | */
15 | style?: React.CSSProperties;
16 |
17 | /**
18 | * Toggles bleed, makes a box full-width on mobile
19 | * @default false
20 | */
21 | bleed?: boolean;
22 |
23 | /**
24 | * Styles the box with light blue color
25 | * @default false
26 | */
27 | info?: boolean;
28 |
29 | /**
30 | * The state of the component, either true for expanded or false for closed.
31 | * @default false
32 | */
33 | expanded?: boolean;
34 |
35 | /**
36 | * Event function to be called any time the component is expanded or closed. Function will be passed a boolean with a value of true if the component is now expanded or false if it is now closed.
37 | */
38 | onChange?: (state: boolean) => void;
39 |
40 | /**
41 | * Component title. Can be a string or component. Used to display the title value which is always present regardless of whether the component is open or closed.
42 | */
43 | title: React.ReactNode;
44 |
45 | /**
46 | * Whether to display the component as a padded box or not.
47 | * @default false
48 | */
49 | box?: boolean;
50 |
51 | /**
52 | * Additional CSS classes to include on the button part of the component
53 | */
54 | buttonClass?: string;
55 |
56 | /**
57 | * Additional CSS classes to include on the content part of the component
58 | */
59 | contentClass?: string;
60 |
61 | /**
62 | * Whether to display the chevron on the button part of the component
63 | * @default true
64 | */
65 | chevron?: boolean;
66 |
67 | /**
68 | * Animate open and close
69 | * @default false
70 | */
71 | animated?: boolean;
72 |
73 | /**
74 | * Wrap the toggle button in a heading element with the specified level.
75 | * If headingLevel is not specified, the button will not be wrapped by a heading element.
76 | */
77 | headingLevel?: HeadingLevel;
78 | };
79 |
--------------------------------------------------------------------------------
/packages/expandable/stories/Expandable.stories.tsx:
--------------------------------------------------------------------------------
1 | import { IconBag16 } from '@fabric-ds/icons/react';
2 | import * as React from 'react';
3 | import { Expandable } from '../src';
4 |
5 | const metadata = { title: 'Layout/Expandable' };
6 | export default metadata;
7 |
8 | export const Default = () => (
9 |
10 | I am expandable
11 |
12 | );
13 |
14 | export const Box = () => (
15 |
16 | I am expandable
17 |
18 | );
19 |
20 | export const BoxWithCustomTitle = () => (
21 |
24 |
25 | This is a title with an icon
26 |
27 | }
28 | box
29 | info
30 | >
31 | I am expandable
32 |
33 | );
34 |
35 | export const InfoBox = () => (
36 |
37 | I am expandable
38 |
39 | );
40 |
41 | export const RedBox = () => (
42 |
43 | I am expandable
44 |
45 | );
46 |
47 | export const GreenButton = () => (
48 |
54 | I am expandable
55 |
56 | );
57 |
58 | export const Controlled = () => {
59 | const [open, setOpen] = React.useState(false);
60 | return (
61 |
62 | I am expandable
63 |
64 | );
65 | };
66 |
67 | export const NoChevron = () => {
68 | const [open, setOpen] = React.useState(false);
69 | return (
70 |
77 | I am expandable
78 |
79 | );
80 | };
81 |
82 | export const Animated = () => {
83 | return (
84 |
85 | I am expandable
86 |
87 | );
88 | };
89 |
90 | export const AnimatedExpanded = () => {
91 | return (
92 |
93 | I am expandable
94 |
95 | );
96 | };
97 |
98 | export const Heading = () => {
99 | return (
100 |
101 | I am expandable
102 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/packages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './_helpers';
2 | export * from './alert/src';
3 | export * from './box/src';
4 | export * from './breadcrumbs/src';
5 | export * from './attention/src';
6 | export * from './button/src';
7 | export * from './pill/src';
8 | export * from './card/src';
9 | export * from './combobox/src';
10 | export * from './expandable/src';
11 | export * from './modal/src';
12 | export * from './select/src';
13 | export * from './slider/src';
14 | export * from './steps/src';
15 | export * from './switch/src';
16 | export * from './tabs/src';
17 | export * from './textarea/src';
18 | export * from './textfield/src';
19 | export * from './toggle/src';
20 |
--------------------------------------------------------------------------------
/packages/modal/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Modal } from './component';
2 |
--------------------------------------------------------------------------------
/packages/modal/src/props.ts:
--------------------------------------------------------------------------------
1 | export type ModalProps = {
2 | /**
3 | * A string or your own custom elements
4 | */
5 | title?: string | JSX.Element | JSX.Element[];
6 |
7 | /**
8 | * Whether the modal is open or not
9 | */
10 | open: boolean;
11 |
12 | /**
13 | * A default back button or your own custom elements
14 | */
15 | left?: boolean | JSX.Element | JSX.Element[];
16 |
17 | /**
18 | * A default close button or your own custom elements
19 | */
20 | right?: boolean | JSX.Element | JSX.Element[];
21 |
22 | /**
23 | * Buttons passed to the footer
24 | */
25 | footer?: JSX.Element | JSX.Element[];
26 |
27 | /**
28 | * Additional classes added to the container
29 | */
30 | className?: string;
31 |
32 | /**
33 | * An id for the container and ARIA attributes. A random id is generated if none is provided.
34 | */
35 | id?: string;
36 |
37 | /**
38 | * Additional styles to the contianer
39 | */
40 | style?: React.CSSProperties;
41 |
42 | /**
43 | * The modal contents
44 | */
45 | children: JSX.Element | JSX.Element[];
46 |
47 | /**
48 | * Handler that is called when the user presses *esc* or clicks outside the modal
49 | */
50 | onDismiss?: () => void;
51 |
52 | /**
53 | * Defines a string value that labels the current element. Must be set if neither `aria-labelledby` or `` is defined,
54 | */
55 | 'aria-label'?: string;
56 |
57 | /**
58 | * Identifies the element (or elements) that labels the current element. Must be set if neither `aria-label` or `` is defined.
59 | */
60 | 'aria-labelledby'?: string;
61 |
62 | /**
63 | * A reference to the element that should be focused. By default it'll be the first interactive element.
64 | */
65 | initialFocusRef?: React.RefObject;
66 | };
67 |
--------------------------------------------------------------------------------
/packages/pill/docs/Pill.mdx:
--------------------------------------------------------------------------------
1 | import { Pill } from '../src';
2 | import { IconSearch16, IconPlus16 } from '@fabric-ds/icons/react';
3 |
4 | # Pill
5 |
6 | Pill is a type of button that is often used as a filter, but can also be used as
7 | a rounded button for overlays, etc.
8 |
9 | ## Import
10 |
11 | ```js
12 | import { Pill } from '@fabric-ds/react';
13 | ```
14 |
15 | ## Examples
16 |
17 | ```jsx example
18 |
19 |
20 |
alert('onClose event')} />
21 |
22 | alert('onClose event')}
27 | />
28 |
29 | ```
30 |
31 | You can also make the Pill clickable like so
32 |
33 | ```jsx example
34 |
35 |
alert('onClick event')} />
36 | alert('onClick event')} suggestion />
37 |
38 | ```
39 |
40 | or have both the label and close button clickable if you wish
41 |
42 | ```jsx example
43 |
44 |
alert('onClick event')}
47 | canClose
48 | onClose={() => alert('onClose event')}
49 | />
50 | alert('onClick event')}
53 | suggestion
54 | canClose
55 | onClose={() => alert('onClose event')}
56 | />
57 |
58 | ```
59 |
60 | ## Icons in Pill
61 |
62 | You can pass any valid HTML as the icon prop, but in this example we will use
63 | icons from `@fabric-ds/icons`
64 |
65 | ```js
66 | import { IconSearch16, IconPlus16 } from '@fabric-ds/icons/react';
67 | ```
68 |
69 | ```jsx example
70 |
71 |
}
73 | onClick={() => alert('onClick event')}
74 | className="py-12"
75 | />
76 |
}
78 | onClick={() => alert('onClick event')}
79 | suggestion
80 | className="py-12"
81 | />
82 |
83 | ```
84 |
85 | ## Props
86 |
87 | ```props packages/pill/src/component.tsx
88 |
89 | ```
90 |
--------------------------------------------------------------------------------
/packages/pill/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import { PillProps } from '.';
4 |
5 | const c = {
6 | pill: 'inline-flex items-center py-8 focus-ring text-12 transition-all',
7 | pillSuggestion:
8 | 'bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 font-bold',
9 | pillFilter: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white',
10 | label: 'pl-12 rounded-l-full',
11 | labelSuggestion: '',
12 | labelFilter: '',
13 | labelWithoutClose: 'pr-12 rounded-r-full',
14 | labelWithClose: 'pr-2',
15 | close: 'pr-12 pl-4 py-10 rounded-r-full',
16 | };
17 |
18 | export function Pill(props: PillProps) {
19 | return (
20 |
21 |
31 | {props.openSRLabel || 'Åpne filter'}
32 | {props.icon || {props.label} }
33 |
34 | {props.canClose && (
35 |
44 |
45 | {props.closeSRLabel || `Fjern filter ${props.label}`}
46 |
47 |
55 |
59 |
60 |
61 | )}
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/packages/pill/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Pill } from './component';
2 | export type { PillProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/pill/src/props.tsx:
--------------------------------------------------------------------------------
1 | export type PillProps = {
2 | /**
3 | * Render text inside of Pill
4 | */
5 | label?: string;
6 |
7 | /**
8 | * Label read by screen readers when targetting a pill
9 | */
10 | openSRLabel?: string;
11 |
12 | /**
13 | * Label read by screen readers when targetting the pill close button
14 | */
15 | closeSRLabel?: string;
16 |
17 | /**
18 | * Render icon inside of Pill
19 | */
20 | icon?: React.ReactNode;
21 |
22 | /**
23 | * Whether Pill should render a closing button, use onClick
24 | * @default false
25 | */
26 | canClose?: boolean;
27 |
28 | /**
29 | * Whether Pill should be rendered as a suggestion
30 | * @default false
31 | */
32 | suggestion?: boolean;
33 |
34 | /**
35 | * Action to be called when the Pill is clicked
36 | */
37 | onClick?: () => void;
38 |
39 | /**
40 | * Action to be called when the close button is clicked
41 | */
42 | onClose?: () => void;
43 |
44 | /**
45 | * Additional styles applied to the Pill
46 | */
47 | className?: string;
48 | };
49 |
--------------------------------------------------------------------------------
/packages/pill/stories/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pill } from '../src';
3 | import { IconSearch16, IconPlus16 } from '@fabric-ds/icons/react';
4 |
5 | const metadata = { title: 'Buttons/Pill' };
6 | export default metadata;
7 |
8 | export const Default = () => {
9 | return ;
10 | };
11 |
12 | export const DefaultCanClose = () => {
13 | return alert('remove')} />;
14 | };
15 |
16 | export const Suggestion = () => {
17 | return ;
18 | };
19 |
20 | export const SuggestionCanClose = () => {
21 | return (
22 | alert('remove')} />
23 | );
24 | };
25 |
26 | export const PillOnClickAndOnClose = () => {
27 | return (
28 | alert('pill is clicked')}
31 | suggestion
32 | canClose
33 | onClose={() => alert('close is clicked')}
34 | />
35 | );
36 | };
37 |
38 | export const PillsWithIcon = () => {
39 | return (
40 |
41 |
}
43 | onClick={() => alert('onClick event')}
44 | className="py-12"
45 | />
46 |
}
48 | onClick={() => alert('onClick event')}
49 | suggestion
50 | className="py-12"
51 | />
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/packages/select/__tests__/Select.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { Select } from '../src';
4 |
5 | describe('Select', () => {
6 | test('supports required prop', () => {
7 | const tree = render( );
8 |
9 | expect(tree.queryByLabelText('label')).toBeRequired();
10 | });
11 |
12 | test('supports disabled prop', () => {
13 | const tree = render( );
14 |
15 | expect(tree.queryByLabelText('label')).toBeDisabled();
16 | expect(tree.container.firstChild).toHaveClass('input--is-disabled');
17 | });
18 |
19 | test('forwards ref to the select element', () => {
20 | let ref = React.createRef();
21 |
22 | const tree = render( );
23 |
24 | expect(tree.queryByRole('combobox')).toEqual(ref.current);
25 | });
26 |
27 | test('logs warning if unlabeled', () => {
28 | console.warn = jest.fn();
29 | render( );
30 | expect(console.warn).toHaveBeenCalled();
31 | });
32 |
33 | test('supports labeling', () => {
34 | const labelText = 'labelText';
35 | const tree = render( );
36 |
37 | const label = tree.getByText(labelText);
38 | const input = tree.getByLabelText(labelText);
39 |
40 | expect(label).toHaveAttribute('for', input.id);
41 | });
42 |
43 | test('supports help text', () => {
44 | const tree = render( );
45 |
46 | const input = tree.getByLabelText('label');
47 | const helpText = tree.getByText('help');
48 |
49 | const helpId = helpText.id;
50 |
51 | expect(helpId).toBeDefined();
52 |
53 | expect(input).toHaveAttribute('aria-describedby', helpId);
54 | });
55 |
56 | test('supports error with help text', () => {
57 | const tree = render( );
58 |
59 | const input = tree.getByLabelText('label');
60 | const errorText = tree.getByRole('alert');
61 |
62 | const errorId = errorText.id;
63 |
64 | expect(errorId).toBeDefined();
65 |
66 | expect(input).toHaveAttribute('aria-invalid', 'true');
67 | expect(input).toHaveAttribute('aria-describedby', errorId);
68 | expect(input).toHaveAttribute('aria-errormessage', errorId);
69 | });
70 |
71 | test('supports error without help text', () => {
72 | const tree = render( );
73 |
74 | const input = tree.getByLabelText('label');
75 |
76 | expect(input).toHaveAttribute('aria-invalid', 'true');
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/packages/select/docs/Select.mdx:
--------------------------------------------------------------------------------
1 | import { Select } from '../src';
2 |
3 | # Select
4 |
5 | A dropdown component for selecting a single value.
6 |
7 | ## Import
8 |
9 | ```js
10 | import { Select } from '@fabric-ds/react';
11 | ```
12 |
13 | ## Example
14 |
15 | ```jsx example
16 |
17 | Strawberries
18 | Raspberries
19 | Cloudberries
20 |
21 | ```
22 |
23 | ## Value
24 |
25 | An initial, uncontrolled, value can be provided using the `defaultValue` prop.
26 | Alternatively, a controlled value can be provided using the `value` prop.
27 |
28 | ```jsx example
29 | function Example() {
30 | let [value, setValue] = React.useState('c');
31 |
32 | return (
33 |
34 |
35 | Strawberries
36 | Raspberries
37 | Cloudberries
38 |
39 |
40 | setValue(event.target.value)}
44 | >
45 | Strawberries
46 | Raspberries
47 | Cloudberries
48 |
49 |
50 | );
51 | }
52 | ```
53 |
54 | ## Labeling
55 |
56 | A visual label should be provided for the Select using the `label` prop.
57 |
58 | ### Accessibility
59 |
60 | If a visible label isn't specified, an `aria-label` should be provided to the
61 | Select for accessibility. If the field is labeled by a separate element, an
62 | `aria-labelledby` prop should be provided using the id of the labeling element
63 | instead.
64 |
65 | ### Optional
66 |
67 | Add the optional prop to indicate that the select is not required.
68 |
69 | ```jsx example
70 |
71 | Strawberries
72 | Raspberries
73 | Cloudberries
74 |
75 | ```
76 |
77 | ## Hint text
78 |
79 | Selects can provide additional context with `hint` if the label and placeholder
80 | aren't enough. You can force the hint text to always display by setting the
81 | `always` prop.
82 |
83 | ```jsx example
84 |
85 | Strawberries
86 | Raspberries
87 | Cloudberries
88 |
89 | ```
90 |
91 | ## Validation
92 |
93 | Selects can communicate to the user whether the current value is invalid.
94 | Implement your own validation logic in your app and set the `invalid` prop to
95 | display it as invalid.
96 |
97 | `invalid` is often paired with `hint` to provide feedback to the user about the
98 | error.
99 |
100 | ```jsx example
101 |
102 | Strawberries
103 | Raspberries
104 | Cloudberries
105 |
106 | ```
107 |
108 | ## Disabled
109 |
110 | Using disabled is considered an anti-pattern and is therefore not supported.
111 | There will ALWAYS be users who don't understand why an element is disabled, or
112 | users who can't even see that it is disabled because of poor lighting conditions
113 | or other reasons. Please consider more informative alternatives.
114 |
115 | ## Props
116 |
117 | ```props packages/select/src/component.tsx
118 |
119 | ```
120 |
--------------------------------------------------------------------------------
/packages/select/src/component.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useId } from '../../utils/src';
3 | import { classNames } from '@chbphone55/classnames';
4 | import type { SelectProps } from './props';
5 |
6 | const setup = (props) => {
7 | const {
8 | className,
9 | invalid,
10 | id,
11 | hint,
12 | always,
13 | label,
14 | style,
15 | optional,
16 | ...rest
17 | } = props;
18 |
19 | const helpId = hint ? `${id}__hint` : undefined;
20 |
21 | return {
22 | attrs: {
23 | div: {
24 | style,
25 | },
26 | label: {
27 | htmlFor: id,
28 | children: label,
29 | },
30 | select: {
31 | ...rest,
32 | 'aria-describedby': helpId,
33 | 'aria-errormessage': invalid && helpId ? helpId : undefined,
34 | 'aria-invalid': invalid,
35 | id,
36 | },
37 | optional,
38 | help:
39 | always || invalid
40 | ? {
41 | children: hint,
42 | id: helpId,
43 | }
44 | : null,
45 | },
46 | classes: classNames(
47 | 'input mb-0',
48 | {
49 | 'input--is-invalid': invalid,
50 | },
51 | className,
52 | ),
53 | };
54 | };
55 |
56 | function Select(props: SelectProps, ref: React.Ref) {
57 | const id = useId(props.id);
58 | const { attrs, classes } = setup({ ...props, id });
59 | const { div, label, select, help, optional } = attrs;
60 |
61 | return (
62 |
63 | {label.children && (
64 |
65 | {label.children}
66 | {optional && (
67 |
68 | (valgfritt)
69 |
70 | )}
71 |
72 | )}
73 |
74 |
75 |
76 | {help &&
}
77 |
78 | );
79 | }
80 |
81 | const _Select = React.forwardRef(Select);
82 | export { _Select as Select };
83 |
--------------------------------------------------------------------------------
/packages/select/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Select } from './component';
2 | export type { SelectProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/select/src/props.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type SelectProps = {
4 | /** Whether the element should receive focus on render. */
5 | autoFocus?: boolean;
6 |
7 | /** The `option` elements to populate the select with. */
8 | children?: React.ReactNode;
9 |
10 | /** Additional CSS class for the container */
11 | className?: string;
12 |
13 | /** The default value (uncontrolled). */
14 | defaultValue?: string;
15 |
16 | /** Renders the field in an invalid state. Often paired together with `hint` to provide feedback about the error. */
17 | invalid?: boolean;
18 |
19 | /** The content to display as the help text. */
20 | hint?: React.ReactNode;
21 |
22 | /** Whether to always show hint */
23 | always?: boolean;
24 |
25 | /** The element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id). */
26 | id?: string;
27 |
28 | /** The content to display as the label. */
29 | label?: React.ReactNode;
30 |
31 | /** The name of the select element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */
32 | name?: string;
33 |
34 | /** Handler that is called when the element loses focus. */
35 | onBlur?: (e: React.FocusEvent) => void;
36 |
37 | /** Handler that is called when the value changes.*/
38 | onChange?: (e: React.ChangeEvent) => void;
39 |
40 | /** Handler that is called when the element receives focus. */
41 | onFocus?: (e: React.FocusEvent) => void;
42 |
43 | /** Whether user input is required on the select before form submission. */
44 | required?: boolean;
45 |
46 | /** Additional CSS styles for the container. */
47 | style?: React.CSSProperties;
48 |
49 | /** The current value (controlled). */
50 | value?: string;
51 |
52 | /** Whether to show optional text */
53 | optional?: boolean;
54 | } & Omit<
55 | React.PropsWithoutRef,
56 | 'onBlur' | 'onChange' | 'onFocus' | 'value' | 'defaultValue'
57 | >;
58 |
--------------------------------------------------------------------------------
/packages/select/stories/Select.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import { Select as FabricSelect } from '../src';
4 |
5 | const metadata = { title: 'Forms/Select' };
6 | export default metadata;
7 |
8 | const Select = (props) => (
9 |
16 | Strawberries
17 | Raspberries
18 | Cloudberries
19 |
20 | );
21 |
22 | export const standard = () => ;
23 |
24 | export const hint = () => (
25 |
26 | );
27 |
28 | export const invalid = () => (
29 |
30 |
31 |
32 |
33 | );
34 |
35 | export const noLabel = () => (
36 |
37 |
43 | Strawberries
44 | Raspberries
45 | Cloudberries
46 |
47 |
48 |
49 | You're berry good at selecting!
50 |
51 |
58 | Strawberries
59 | Raspberries
60 | Cloudberries
61 |
62 |
63 | );
64 |
65 | export const optional = () => ;
66 |
--------------------------------------------------------------------------------
/packages/slider/docs/Slider.mdx:
--------------------------------------------------------------------------------
1 | import { Slider } from '../src/component';
2 |
3 | # Slider
4 |
5 | A slider is an input where the user selects a value from within a given range.
6 | The precise value, however, is not necessarily considered important. It can have
7 | single or dual drag handles.
8 |
9 | ## Import
10 |
11 | ```js
12 | import { Slider } from '@fabric-ds/react';
13 | ```
14 |
15 | ## Example
16 |
17 | ```jsx example
18 | function Example() {
19 | const [value, setValue] = React.useState(2_500_000);
20 |
21 | return (
22 |
23 | {value}
24 | setValue(value)}
26 | value={value}
27 | min={1000}
28 | max={10_000_000}
29 | step={1000}
30 | />
31 |
32 | );
33 | }
34 | ```
35 |
36 | ## Accessiblity
37 |
38 | To be accessible, an `aria-label` prop should be provided to the slider. If the
39 | slider is labeled by a separate element, use an `aria-labelledby` prop with the
40 | id of the labeling element instead.
41 |
42 | ## Events
43 |
44 | The slider accepts an `onChange` prop which is triggered whenever the value is
45 | changed by the user. Note that this value updates as the user is dragging.
46 |
47 | ## Visual options
48 |
49 | ### Disabled
50 |
51 | ```jsx example
52 |
53 |
54 |
55 | ```
56 |
57 | ## Props
58 |
59 | ```props packages/slider/src/component.tsx
60 |
61 | ```
62 |
--------------------------------------------------------------------------------
/packages/slider/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Slider } from './component';
2 | export type { SliderProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/slider/src/props.ts:
--------------------------------------------------------------------------------
1 | export type SliderProps = {
2 | /**
3 | * The greatest value in the range of permitted values
4 | * @default 100
5 | */
6 | max: number;
7 |
8 | /**
9 | * The lowest value in the range of permitted values
10 | * @default 0
11 | */
12 | min: number;
13 |
14 | /** Specifies the value granularity */
15 | step: number;
16 |
17 | /** The current value */
18 | value: number;
19 |
20 | /** Whether the slider is disabled */
21 | disabled?: boolean;
22 |
23 | /** Handler that is called when the value of the slider changes */
24 | onChange?: (value: number) => void;
25 |
26 | /** String value that labels the slider */
27 | 'aria-label'?: string;
28 |
29 | /** Identifies the element that labels the slider */
30 | 'aria-labelledby'?: string;
31 |
32 | /** Human readable text alternative for the value */
33 | 'aria-valuetext'?: string;
34 | };
35 |
--------------------------------------------------------------------------------
/packages/slider/stories/Slider.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slider } from '../src';
3 |
4 | const metadata = { title: 'Forms/Slider' };
5 | export default metadata;
6 |
7 | export const Regular = () => {
8 | const [value, setValue] = React.useState(2_500_000);
9 |
10 | return (
11 |
12 | {value}
13 | setValue(val)}
15 | value={value}
16 | min={1000}
17 | max={10_000_000}
18 | step={1000}
19 | />
20 | setValue(2_500_000)}>Reset
21 |
22 | );
23 | };
24 |
25 | export const Disabled = () => {
26 | const [value, setValue] = React.useState(625_000);
27 | return (
28 |
29 | {value}
30 | setValue(value)}
32 | value={value}
33 | disabled
34 | min={1000}
35 | max={10_000_000}
36 | step={1000}
37 | />
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/packages/steps/docs/Steps.mdx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Steps, Step } from '../src';
3 | import { Button } from '../../button/src';
4 |
5 | # Steps
6 |
7 | The steps component is built to handle user journeys, making it clear to the end
8 | user where they are in the process.
9 |
10 | ## Import
11 |
12 | ```js
13 | import { Steps, Step } from '@fabric-ds/react';
14 | ```
15 |
16 | ## Examples
17 |
18 | ```jsx example
19 | function Example() {
20 | return (
21 |
22 |
23 | Step one
24 | Content
25 |
26 |
27 | Step two
28 | Content
29 |
30 |
31 | Step three
32 | Content
33 |
34 |
35 | );
36 | }
37 | ```
38 |
39 | **Note** the `active` and `completed` properties on the `` component. You
40 | have to conditionally set the truthiness of these properties depending on where
41 | in the process the user is. See interactive example below.
42 |
43 | ### Interactive example
44 |
45 | ```jsx example
46 | function Example() {
47 | const [state, setState] = useState(0);
48 | const [horizontal, setHorizontal] = useState(false);
49 | const [right, setRight] = useState(false);
50 |
51 | const handleIncrease = () => {
52 | if (state === 3) {
53 | return setState(0);
54 | }
55 |
56 | return setState((state) => state + 1);
57 | };
58 |
59 | return (
60 | <>
61 |
62 | Complete a step
63 |
64 | setHorizontal(!horizontal)} small>
65 | {horizontal ? 'Vertical' : 'Horizontal'}
66 |
67 | {!horizontal && (
68 | setRight(!right)} small>
69 | {right ? 'Left' : 'Right'}
70 |
71 | )}
72 |
73 |
74 | 0}>
75 | Step one
76 | Some content
77 |
78 | 1}>
79 | Step two
80 | Some content
81 |
82 | 2}>
83 | Step three
84 | Some content
85 |
86 |
87 | >
88 | );
89 | }
90 | ```
91 |
92 | **Note** the `horizontal` and `right` properties on the `` component.
93 | These choose the direction and alignment of the steps.
94 |
95 | ## Steps Props
96 |
97 | ```props packages/steps/src/component.tsx
98 |
99 | ```
100 |
101 | ## Step Props
102 |
103 | ```props packages/steps/src/step.tsx
104 |
105 | ```
106 |
--------------------------------------------------------------------------------
/packages/steps/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext } from 'react';
2 | import { StepsProps } from './props';
3 | import { classNames } from '@chbphone55/classnames';
4 |
5 | export const StepsContext = createContext<{
6 | horizontal?: boolean;
7 | right?: boolean;
8 | }>({
9 | horizontal: undefined,
10 | right: undefined,
11 | });
12 |
13 | export function Steps(props: StepsProps) {
14 | return (
15 |
21 |
27 | {props.children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/steps/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Steps } from './component';
2 | export { Step } from './step';
3 | export type { StepsProps } from './props';
4 | export type { StepProps } from './step';
5 |
--------------------------------------------------------------------------------
/packages/steps/src/props.tsx:
--------------------------------------------------------------------------------
1 | export interface StepsProps {
2 | /**
3 | * Direction of steps
4 | * @default false
5 | */
6 | horizontal?: boolean;
7 |
8 | /**
9 | * Align steps to the right
10 | * @default false
11 | */
12 | right?: boolean;
13 |
14 | /**
15 | * Two or more `Step` components
16 | */
17 | children: JSX.Element[];
18 |
19 | /**
20 | * Additional CSS class for the container
21 | */
22 | className?: string;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/steps/src/step.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { classNames } from '@chbphone55/classnames';
3 | import { step as c } from '@fabric-ds/css/component-classes';
4 | import { useContext } from 'react';
5 | import { StepsContext } from './component';
6 |
7 | export interface StepProps {
8 | /**
9 | * Step is active
10 | * @default false
11 | */
12 | active?: boolean;
13 |
14 | /**
15 | * Step is completed
16 | * @default false
17 | */
18 | completed?: boolean;
19 |
20 | /**
21 | * Contents of Step
22 | */
23 | children: JSX.Element | JSX.Element[];
24 | }
25 |
26 | export function Step({ active, completed, children }: StepProps) {
27 | const StepsProps = useContext(StepsContext);
28 | const vertical = !StepsProps.horizontal;
29 | const left = !StepsProps.right;
30 |
31 | return (
32 |
41 | {!vertical && (
42 |
51 | )}
52 |
81 |
93 |
100 | {children}
101 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/packages/steps/stories/Steps.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Steps, Step } from '../src';
3 | import { Button } from '../../button/src';
4 |
5 | const metadata = { title: 'Navigation/Steps' };
6 | export default metadata;
7 |
8 | export const Vertical = () => {
9 | return (
10 |
11 |
12 | Step one
13 | Content
14 |
15 |
16 | Step two
17 | Content
18 |
19 |
20 | Step three
21 | Content
22 |
23 |
24 | );
25 | };
26 |
27 | export const WithProgress = () => {
28 | return (
29 |
30 |
31 | Step one
32 | Content
33 |
34 |
35 | Step two
36 | Content
37 |
38 |
39 | Step three
40 | Content
41 |
42 |
43 | );
44 | };
45 |
46 | export const RightAligned = () => {
47 | return (
48 |
49 |
50 | Step one
51 | Content
52 |
53 |
54 | Step two
55 | Content
56 |
57 |
58 | Step three
59 | Content
60 |
61 |
62 | );
63 | };
64 |
65 | export const Horizontal = () => {
66 | return (
67 |
68 |
69 | Step one
70 | Content
71 |
72 |
73 | Step two
74 | Content
75 |
76 |
77 | Step three
78 | Content
79 |
80 |
81 | );
82 | };
83 | export const Interactive = () => {
84 | const [state, setState] = useState(0);
85 |
86 | const handleIncrease = () => {
87 | if (state === 4) {
88 | return setState(0);
89 | }
90 |
91 | return setState((state) => state + 1);
92 | };
93 |
94 | return (
95 | <>
96 |
97 | Complete a step
98 |
99 |
100 |
101 | 0}>
102 | Step one
103 | Some content
104 |
105 | 1}>
106 | Step two
107 | Some content
108 |
109 | 2}>
110 | Step three
111 | Some content
112 |
113 | 3}>
114 | Step four
115 | Some content
116 |
117 |
118 | >
119 | );
120 | };
121 |
--------------------------------------------------------------------------------
/packages/switch/docs/Switch.mdx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Switch } from '../src';
3 |
4 | # Switch
5 |
6 | This component allows users to toggle between validity for a condition.
7 |
8 | ## Import
9 |
10 | ```js
11 | import { Switch } from '@fabric-ds/react';
12 | ```
13 |
14 | ## Examples
15 |
16 | ```jsx example
17 | function Example() {
18 | const [value, setValue] = useState(false);
19 |
20 | return (
21 | setValue(!value)}
24 | aria-label="Toggle switch"
25 | />
26 | );
27 | }
28 | ```
29 |
30 | #### Writing your own custom click handler
31 |
32 | ```jsx example
33 | function Example() {
34 | const [value, setValue] = useState(false);
35 |
36 | return (
37 | {
40 | setValue(!value);
41 | alert('Your own custom action when toggled');
42 | }}
43 | aria-label="Toggle switch"
44 | />
45 | );
46 | }
47 | ```
48 |
49 | ## Accessibility
50 |
51 | The Switch needs either `aria-label` or `aria-labelledby` to be accessible to
52 | screen readers.
53 |
54 | ###
55 |
56 | #### Disabled property
57 |
58 | There is no visual styling to a disabled button. It is recommended to display a
59 | message to the user, for example a modal or toast, stating why the user cannot
60 | toggle the switch.
61 |
62 | ## Props
63 |
64 | ```props packages/switch/src/component.tsx
65 |
66 | ```
67 |
--------------------------------------------------------------------------------
/packages/switch/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SwitchProps } from './props';
3 | import { classNames } from '@chbphone55/classnames';
4 | import { switchToggle as c } from '@fabric-ds/css/component-classes';
5 |
6 | export function Switch({
7 | id,
8 | value,
9 | onClick,
10 | 'aria-label': ariaLabel,
11 | 'aria-labelledby': ariaLabelledBy,
12 | ...attrs
13 | }: SwitchProps) {
14 | const switchFocus =
15 | 'focus:outline-none focus:ring ring-offset-1 ring-blue-200 rounded-full';
16 |
17 | return (
18 |
19 |
30 |
36 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/switch/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Switch } from './component';
2 | export type { SwitchProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/switch/src/props.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface SwitchProps {
4 | /**
5 | * The unique identifier
6 | */
7 | id?: string;
8 |
9 | /**
10 | * The value of the Switch
11 | */
12 | value: boolean;
13 |
14 | /**
15 | * Handler for when the Switch is clicked
16 | */
17 | onClick: (e?: React.MouseEvent) => void;
18 |
19 | /**
20 | * Defines a string value that labels the current element. Must be set if `aria-labelledby` is not defined,
21 | */
22 | 'aria-label'?: string;
23 |
24 | /**
25 | * Identifies the element (or elements) that labels the current element. Must be set if `aria-label` is not defined.
26 | */
27 | 'aria-labelledby'?: string;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/switch/stories/Switch.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Switch } from '../src';
3 |
4 | const metadata = { title: 'Forms/Switch' };
5 | export default metadata;
6 |
7 | export const DefaultDisabled = () => {
8 | const [value, setValue] = useState(false);
9 |
10 | return (
11 | setValue(!value)}
14 | value={value}
15 | />
16 | );
17 | };
18 |
19 | export const DefaultEnabled = () => {
20 | const [value, setValue] = useState(true);
21 |
22 | return (
23 | setValue(!value)}
26 | value={value}
27 | />
28 | );
29 | };
30 |
31 | export const CustomClickHandler = () => {
32 | const [value, setValue] = useState(false);
33 |
34 | const handleClick = () => {
35 | const newValue = !value;
36 | setValue(newValue);
37 | alert(`Custom click handler: Switch ${newValue ? 'enabled' : 'disabled'}.`);
38 | };
39 |
40 | return (
41 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/packages/tabs/__tests__/Tabs.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import * as React from 'react';
3 | import { Tab, Tabs } from '../src';
4 | import { runAxe } from '../../../test/test-helpers';
5 |
6 | test('should render component with 3 tabs', () => {
7 | expect(
8 | render(
9 |
10 | Content 1
11 | Content 2
12 | Content 3
13 | ,
14 | ).container.firstChild,
15 | ).toMatchSnapshot();
16 | });
17 |
18 | test('centered', () => {
19 | expect(
20 | render(
21 |
22 | Content 1
23 | Content 2
24 | Content 3
25 | ,
26 | ).container.firstChild,
27 | ).toMatchSnapshot();
28 | });
29 |
30 | test('should set tab 3 as active', () => {
31 | expect(
32 | render(
33 |
34 | Content 1
35 | Content 2
36 | Content 3
37 | ,
38 | ).container.firstChild,
39 | ).toMatchSnapshot();
40 | });
41 |
42 | test('should be accessible', async () => {
43 | await runAxe(
44 |
45 | Content 1
46 | Content 2
47 | Content 3
48 | ,
49 | );
50 | });
51 |
--------------------------------------------------------------------------------
/packages/tabs/src/component-tab-panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { tab as c } from '@fabric-ds/css/component-classes';
3 | import type { TabPanelProps } from './props';
4 |
5 | export function TabPanel(props: TabPanelProps) {
6 | const { children, name, hidden, ...rest } = props;
7 |
8 | return (
9 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/tabs/src/component-tab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { classNames as cn } from '@chbphone55/classnames';
3 | import { tab as c } from '@fabric-ds/css/component-classes';
4 | import type { TabProps } from './props';
5 |
6 | const setup = ({
7 | className,
8 | isActive,
9 | setActive,
10 | contained,
11 | ...rest
12 | }: any) => ({
13 | tab: cn({
14 | [className]: !!className,
15 | [c.tab]: true,
16 | [c.tabActive]: isActive,
17 | [c.tabContained]: contained,
18 | [c.tabContainedActive]: contained && isActive,
19 | }),
20 | icon: cn({
21 | [c.icon]: true,
22 | [c.iconUnderlined]: !contained,
23 | [isActive ? c.iconUnderlinedActive : c.iconUnderlinedInactive]: !contained,
24 | }),
25 | content: cn({
26 | [c.contentUnderlined]: !contained,
27 | [isActive ? c.contentUnderlinedActive : c.contentUnderlinedInactive]:
28 | !contained,
29 | [c.contentContainedActive]: contained && isActive,
30 | }),
31 | attrs: { ...rest },
32 | });
33 |
34 | export function Tab(props: TabProps) {
35 | const {
36 | children,
37 | label,
38 | setActive = () => {},
39 | name,
40 | onClick,
41 | isActive,
42 | } = props;
43 | const { tab, icon, content, attrs } = setup(props);
44 | const { over, ...rest } = attrs;
45 |
46 | const handleClick = (e) => {
47 | setActive(name);
48 | onClick && onClick(e);
49 | };
50 |
51 | return (
52 |
63 | {!children && {label} }
64 |
65 | {children && over && (
66 | <>
67 | {children}
68 | {label}
69 | >
70 | )}
71 |
72 | {children && !over && (
73 |
74 | {children}
75 | {label}
76 |
77 | )}
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/packages/tabs/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Tab } from './component-tab';
2 | export { TabPanel } from './component-tab-panel';
3 | export { Tabs } from './component-tabs';
4 | export type { TabProps, TabPanelProps, TabsProps } from './props';
5 |
--------------------------------------------------------------------------------
/packages/tabs/src/props.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type TabsProps = {
4 | /**
5 | * Whether the tabs should use the contained look and feel or not.
6 | * @default false
7 | */
8 | contained?: boolean;
9 |
10 | /**
11 | * The Tabs within the container.
12 | */
13 | children: React.ReactNode;
14 |
15 | /**
16 | * Used to set the name of the Tab that should be active on mount.
17 | * Defaults to the first tab if not present and isActive is not set on any Tab.
18 | */
19 | active?: string;
20 |
21 | /** Additional CSS class for the container */
22 | className?: string;
23 |
24 | /** Handler that is called when the tab changes. */
25 | onChange?: (name: string) => void;
26 |
27 | /** Additional CSS styles for the container. */
28 | style?: React.CSSProperties;
29 | };
30 |
31 | export type TabProps = {
32 | setActive?: (name: string) => void;
33 |
34 | /** Additional CSS class for the tab. */
35 | className?: string;
36 |
37 | /**
38 | * Set the over prop to true if you need to move icons to above the tab label
39 | * @default false
40 | */
41 | over?: boolean;
42 |
43 | /** Additional content to be included in the tab (eg. icons). Content is placed above the label. */
44 | children?: React.ReactNode;
45 |
46 | /** Tab name identifier. This value will be omitted as the argument to the Tabs onChange handler. */
47 | name: string;
48 |
49 | /** The label of the tab item. */
50 | label: React.ReactNode;
51 |
52 | /** Used to set which tab should be active on mount. Defaults to the first tab if not present. */
53 | isActive?: boolean;
54 |
55 | /** Additional CSS styles for the tab. */
56 | style?: React.CSSProperties;
57 |
58 | /**
59 | * Action to be called when the component is clicked
60 | */
61 | onClick?: (e: React.MouseEvent) => void;
62 | };
63 |
64 | export type TabPanelProps = {
65 | children?: React.ReactNode;
66 |
67 | /** Tab name identifier. Must exactly match the name identifier of a Tab. */
68 | name: string;
69 |
70 | /** Show/hide panel manually (in server-side rendering). */
71 | hidden?: boolean;
72 | };
73 |
--------------------------------------------------------------------------------
/packages/tabs/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function debounce(func, wait = 200, immediate = false) {
2 | let timeout;
3 | return function () {
4 | var later = () => {
5 | timeout = null;
6 | // @ts-ignore
7 | if (!immediate) func.apply(this, arguments);
8 | };
9 | let callNow = immediate && !timeout;
10 | clearTimeout(timeout);
11 | timeout = setTimeout(later, wait);
12 | // @ts-ignore
13 | if (callNow) func.apply(this, arguments);
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/packages/textarea/__tests__/TextArea.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { TextArea } from '../src';
4 |
5 | // TODO: Write tests for `maximumRows` and `minimumRows`
6 |
7 | describe('TextArea', () => {
8 | test('renders with placeholder text', () => {
9 | const placeholder = 'placeholder';
10 | const tree = render(
11 | ,
12 | );
13 | const input = tree.getByPlaceholderText(placeholder);
14 | expect(input).toBeTruthy();
15 | // @ts-ignore
16 | expect(input.placeholder).toBe(placeholder);
17 | });
18 |
19 | test('supports required prop', () => {
20 | const tree = render();
21 |
22 | expect(tree.queryByLabelText('label')).toBeRequired();
23 | });
24 |
25 | test('supports disabled prop', () => {
26 | const tree = render();
27 |
28 | expect(tree.queryByLabelText('label')).toBeDisabled();
29 | expect(tree.container.firstChild).toHaveClass('input--is-disabled');
30 | });
31 |
32 | test('supports readOnly prop', () => {
33 | const tree = render();
34 |
35 | expect(tree.queryByLabelText('label')).toHaveAttribute('readOnly');
36 | expect(tree.container.firstChild).toHaveClass('input--is-read-only');
37 | });
38 |
39 | test('forwards ref to the textarea element', () => {
40 | let ref = React.createRef();
41 |
42 | const tree = render();
43 |
44 | expect(tree.queryByRole('textbox')).toEqual(ref.current);
45 | });
46 |
47 | test('logs warning if unlabeled', () => {
48 | console.warn = jest.fn();
49 | render();
50 | expect(console.warn).toHaveBeenCalled();
51 | });
52 |
53 | test('supports labeling', () => {
54 | const labelText = 'labelText';
55 | const tree = render();
56 |
57 | const label = tree.getByText(labelText);
58 | const input = tree.getByLabelText(labelText);
59 |
60 | expect(label).toHaveAttribute('for', input.id);
61 | });
62 |
63 | test('supports help text', () => {
64 | const tree = render();
65 |
66 | const input = tree.getByLabelText('label');
67 | const helpText = tree.getByText('help');
68 |
69 | const helpId = helpText.id;
70 |
71 | expect(helpId).toBeDefined();
72 |
73 | expect(input).toHaveAttribute('aria-describedby', helpId);
74 | });
75 |
76 | test('supports error with help text', () => {
77 | const tree = render();
78 |
79 | const input = tree.getByLabelText('label');
80 | const errorText = tree.getByRole('alert');
81 |
82 | const errorId = errorText.id;
83 |
84 | expect(errorId).toBeDefined();
85 |
86 | expect(input).toHaveAttribute('aria-invalid', 'true');
87 | expect(input).toHaveAttribute('aria-describedby', errorId);
88 | expect(input).toHaveAttribute('aria-errormessage', errorId);
89 | });
90 |
91 | test('supports error without help text', () => {
92 | const tree = render();
93 |
94 | const input = tree.getByLabelText('label');
95 |
96 | expect(input).toHaveAttribute('aria-invalid', 'true');
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/packages/textarea/src/component.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '@chbphone55/classnames';
2 | import React, { forwardRef, useRef } from 'react';
3 | import { useId } from '../../utils/src';
4 | import { TextAreaProps } from './props';
5 | import useTextAreaHeight from './useTextAreaHeight';
6 |
7 | /**
8 | * A textarea component that automatically resizes as content changes.
9 | */
10 | export const TextArea = forwardRef(
11 | (props, forwardRef) => {
12 | const {
13 | className,
14 | disabled,
15 | error,
16 | helpText,
17 | id: providedId,
18 | invalid,
19 | label,
20 | maximumRows,
21 | minimumRows,
22 | readOnly,
23 | style,
24 | value,
25 | optional,
26 | ...rest
27 | } = props;
28 |
29 | const id = useId(providedId);
30 | const ref = useRef(null);
31 |
32 | const helpId = helpText ? `${id}__hint` : undefined;
33 | const isInvalid = invalid ?? error;
34 |
35 | useTextAreaHeight({
36 | ref,
37 | value,
38 | maximumRows,
39 | minimumRows,
40 | });
41 |
42 | return (
43 |
52 | {label && (
53 |
54 | {label}
55 | {optional && (
56 |
57 | (valgfritt)
58 |
59 | )}
60 |
61 | )}
62 |
85 | );
86 | },
87 | );
88 |
--------------------------------------------------------------------------------
/packages/textarea/src/index.ts:
--------------------------------------------------------------------------------
1 | export { TextArea } from './component';
2 | export type { TextAreaProps } from './props';
3 |
--------------------------------------------------------------------------------
/packages/textarea/src/props.ts:
--------------------------------------------------------------------------------
1 | export type TextAreaProps = {
2 | /** Whether the element should receive focus on render. */
3 | autoFocus?: boolean;
4 |
5 | /** Additional CSS class for the container */
6 | className?: string;
7 |
8 | /** The default value (uncontrolled). */
9 | defaultValue?: string;
10 |
11 | /** Whether the input is disabled. */
12 | disabled?: boolean;
13 |
14 | /**
15 | * Renders the field in an invalid state. Often paired together with `helpText` to provide feedback about the error.
16 | *
17 | * @deprecated use `invalid` instead.
18 | */
19 | error?: boolean;
20 |
21 | /** Renders the field in an invalid state. Often paired together with `helpText` to provide feedback about the error. */
22 | invalid?: boolean;
23 |
24 | /** The content to display as the help text. */
25 | helpText?: React.ReactNode;
26 |
27 | /** The element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id). */
28 | id?: string;
29 |
30 | /** The content to display as the label. */
31 | label?: string;
32 |
33 | /** Handler that is called when the element loses focus. */
34 | onBlur?: (e: React.FocusEvent) => void;
35 |
36 | /** Handler that is called when the value changes.*/
37 | onChange?: (e: React.ChangeEvent) => void;
38 |
39 | /** Handler that is called when the element receives focus. */
40 | onFocus?: (e: React.FocusEvent) => void;
41 |
42 | /** Maximum number of text rows upto which the input can grow. */
43 | maximumRows?: number;
44 |
45 | /** Minimum number of text rows to show for the input. */
46 | minimumRows?: number;
47 |
48 | /** The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */
49 | name?: string;
50 |
51 | /** Text hint that occupies the text input when it is empty. */
52 | placeholder?: string;
53 |
54 | /** Whether the input can be selected but not changed by the user. */
55 | readOnly?: boolean;
56 |
57 | /** Whether user input is required on the input before form submission. */
58 | required?: boolean;
59 |
60 | /** Additional CSS styles for the container. */
61 | style?: React.CSSProperties;
62 |
63 | /** The current value (controlled). */
64 | value?: string;
65 |
66 | /** Whether to show optional text */
67 | optional?: boolean;
68 | } & Omit<
69 | React.PropsWithoutRef,
70 | // omit these, otherwise they seem to form a union type (in the prop table docs)
71 | 'onBlur' | 'onFocus' | 'onChange' | 'value' | 'defaultValue'
72 | >;
73 |
--------------------------------------------------------------------------------
/packages/textarea/src/useTextAreaHeight.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from '../../utils/src';
2 | import React, { useEffect, useRef } from 'react';
3 |
4 | type Params = {
5 | ref: React.RefObject;
6 | value?: string;
7 | /** The minimum number of rows of text to display */
8 | minimumRows?: number;
9 | /** The maximum number of rows of text to display */
10 | maximumRows?: number;
11 | };
12 |
13 | export default function useTextAreaHeight({
14 | ref,
15 | value,
16 | minimumRows,
17 | maximumRows,
18 | }: Params): void {
19 | const minHeight = useRef(-Infinity);
20 | const maxHeight = useRef(Infinity);
21 |
22 | const isControlled = value !== undefined;
23 |
24 | function resize(textarea: HTMLTextAreaElement) {
25 | textarea.style.setProperty('height', 'auto');
26 |
27 | let height = Math.max(minHeight.current, textarea.scrollHeight);
28 |
29 | height = Math.min(maxHeight.current, height);
30 |
31 | textarea.style.setProperty('height', height + 'px');
32 | }
33 |
34 | // Calculate the minimum and maximal heights
35 | useLayoutEffect(() => {
36 | if (ref.current && (minimumRows || maximumRows)) {
37 | const style = getComputedStyle(ref.current);
38 |
39 | const lineHeight = parseFloat(style.getPropertyValue('line-height'));
40 |
41 | const topPadding = parseFloat(style.getPropertyValue('padding-top'));
42 | const bottomPadding = parseFloat(
43 | style.getPropertyValue('padding-bottom'),
44 | );
45 | const bottomBorder = parseFloat(
46 | style.getPropertyValue('border-bottom-width'),
47 | );
48 | const offset = topPadding + bottomPadding + bottomBorder;
49 |
50 | if (minimumRows) {
51 | minHeight.current = lineHeight * minimumRows + offset;
52 | }
53 | if (maximumRows) {
54 | maxHeight.current = lineHeight * maximumRows + offset;
55 | }
56 | }
57 | }, [ref, maximumRows, minimumRows]);
58 |
59 | /**
60 | * This handles both the initial sizing and resizing when the value changes for a controlled component
61 | */
62 | useLayoutEffect(() => {
63 | if (ref.current) {
64 | resize(ref.current);
65 | }
66 | }, [ref, value]);
67 |
68 | /**
69 | * Resizing for uncontrolled textareas
70 | */
71 | useEffect(() => {
72 | if (!ref.current && isControlled) return;
73 |
74 | const textarea = ref.current as HTMLTextAreaElement;
75 |
76 | const handleInput = () => {
77 | resize(textarea);
78 | };
79 |
80 | textarea.addEventListener('input', handleInput);
81 |
82 | return () => textarea.removeEventListener('input', handleInput);
83 | }, [ref, isControlled]);
84 | }
85 |
--------------------------------------------------------------------------------
/packages/textarea/stories/TextArea.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TextArea } from '../src';
3 |
4 | const metadata = { title: 'Forms/TextArea' };
5 | export default metadata;
6 |
7 | export const standard = () => (
8 |
9 | );
10 |
11 | export const valueUncontrolled = () => (
12 |
13 | );
14 |
15 | export const ValueControlled = () => {
16 | const [value, setValue] = React.useState('Test');
17 | return (
18 |