├── .browserslistrc ├── .editorconfig ├── .env.dist ├── .eslintrc ├── .github ├── auto_assign.yml ├── markdown-link-check.json ├── pr-labeler.yml ├── release-drafter.yml └── workflows │ ├── broken-links-check.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── git.yml │ ├── lint.yml │ ├── pull-request-meta.yml │ ├── release-management.yml │ └── test.yml ├── .gitignore ├── .markdownlint.jsonc ├── .npmignore ├── .nvmrc ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASING.md ├── babel.config.js ├── docker-compose.yml ├── docker ├── mkdocs │ └── Dockerfile └── node │ └── Dockerfile ├── jest.config.js ├── mkdocs.yml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── racom.svg ├── src ├── CNAME ├── components │ ├── Alert │ │ ├── Alert.jsx │ │ ├── Alert.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── Alert.test.jsx │ │ ├── _settings.scss │ │ ├── _theme.scss │ │ └── index.js │ ├── Badge │ │ ├── Badge.jsx │ │ ├── Badge.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── Badge.test.jsx │ │ ├── _settings.scss │ │ └── index.js │ ├── Button │ │ ├── Button.jsx │ │ ├── Button.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── Button.test.jsx │ │ ├── _settings.scss │ │ ├── _theme.scss │ │ ├── _tools.scss │ │ ├── helpers │ │ │ ├── getRootLabelVisibilityClassName.js │ │ │ └── getRootPriorityClassName.js │ │ └── index.js │ ├── ButtonGroup │ │ ├── ButtonGroup.jsx │ │ ├── ButtonGroup.module.scss │ │ ├── ButtonGroupContext.js │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── ButtonGroup.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Card │ │ ├── Card.jsx │ │ ├── Card.module.scss │ │ ├── CardBody.jsx │ │ ├── CardFooter.jsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Card.test.jsx │ │ │ ├── CardBody.test.jsx │ │ │ └── CardFooter.test.jsx │ │ ├── _settings.scss │ │ ├── _theme.scss │ │ └── index.js │ ├── CheckboxField │ │ ├── CheckboxField.jsx │ │ ├── CheckboxField.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── CheckboxField.test.jsx │ │ └── index.js │ ├── FileInputField │ │ ├── FileInputField.jsx │ │ ├── FileInputField.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── FileInputField.test.jsx │ │ ├── _settings.scss │ │ └── index.js │ ├── FormLayout │ │ ├── FormLayout.jsx │ │ ├── FormLayout.module.scss │ │ ├── FormLayoutContext.js │ │ ├── FormLayoutCustomField.jsx │ │ ├── FormLayoutCustomField.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── FormLayout.test.jsx │ │ │ └── FormLayoutCustomField.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Grid │ │ ├── Grid.jsx │ │ ├── Grid.module.scss │ │ ├── GridSpan.jsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Grid.test.jsx │ │ │ └── GridSpan.test.jsx │ │ ├── _helpers │ │ │ ├── __tests__ │ │ │ │ └── generateResponsiveCustomProperties.test.js │ │ │ └── generateResponsiveCustomProperties.js │ │ ├── _settings.scss │ │ ├── _tools.scss │ │ └── index.js │ ├── InputGroup │ │ ├── InputGroup.jsx │ │ ├── InputGroup.module.scss │ │ ├── InputGroupContext.js │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── InputGroup.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Modal │ │ ├── Modal.jsx │ │ ├── Modal.module.scss │ │ ├── ModalBody.jsx │ │ ├── ModalBody.module.scss │ │ ├── ModalCloseButton.jsx │ │ ├── ModalCloseButton.module.scss │ │ ├── ModalContent.jsx │ │ ├── ModalContent.module.scss │ │ ├── ModalFooter.jsx │ │ ├── ModalFooter.module.scss │ │ ├── ModalHeader.jsx │ │ ├── ModalHeader.module.scss │ │ ├── ModalTitle.jsx │ │ ├── ModalTitle.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Modal.test.jsx │ │ │ ├── ModalBody.test.jsx │ │ │ ├── ModalCloseButton.test.jsx │ │ │ ├── ModalContent.test.jsx │ │ │ ├── ModalFooter.test.jsx │ │ │ ├── ModalHeader.test.jsx │ │ │ └── ModalTitle.test.jsx │ │ ├── _animations.scss │ │ ├── _helpers │ │ │ ├── dialogOnCancelHandler.js │ │ │ ├── dialogOnClickHandler.js │ │ │ ├── dialogOnCloseHandler.js │ │ │ ├── dialogOnKeyDownHandler.js │ │ │ ├── getJustifyClassName.js │ │ │ ├── getPositionClassName.js │ │ │ ├── getScrollingClassName.js │ │ │ └── getSizeClassName.js │ │ ├── _hooks │ │ │ ├── useModalFocus.js │ │ │ └── useModalScrollPrevention.js │ │ ├── _settings.scss │ │ ├── _theme.scss │ │ └── index.js │ ├── Paper │ │ ├── Paper.jsx │ │ ├── Paper.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── Paper.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Popover │ │ ├── Popover.jsx │ │ ├── Popover.module.scss │ │ ├── PopoverWrapper.jsx │ │ ├── PopoverWrapper.module.scss │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Popover.test.jsx │ │ │ └── PopoverWrapper.test.jsx │ │ ├── _helpers │ │ │ ├── cleanPlacementStyle.js │ │ │ ├── getRootAlignmentClassName.js │ │ │ └── getRootSideClassName.js │ │ ├── _theme.scss │ │ └── index.js │ ├── Radio │ │ ├── README.md │ │ ├── Radio.jsx │ │ ├── Radio.module.scss │ │ ├── __tests__ │ │ │ └── Radio.test.jsx │ │ └── index.js │ ├── ScrollView │ │ ├── README.md │ │ ├── ScrollView.jsx │ │ ├── ScrollView.module.scss │ │ ├── __tests__ │ │ │ └── ScrollView.test.jsx │ │ ├── _helpers │ │ │ └── getElementsPositionDifference.js │ │ ├── _hooks │ │ │ ├── useLoadResizeHook.js │ │ │ └── useScrollPositionHook.js │ │ └── index.js │ ├── SelectField │ │ ├── README.md │ │ ├── SelectField.jsx │ │ ├── SelectField.module.scss │ │ ├── __tests__ │ │ │ └── SelectField.test.jsx │ │ ├── _components │ │ │ └── Option │ │ │ │ ├── Option.jsx │ │ │ │ └── index.js │ │ └── index.js │ ├── Table │ │ ├── README.md │ │ ├── Table.jsx │ │ ├── Table.module.scss │ │ ├── __tests__ │ │ │ └── Table.test.jsx │ │ ├── _components │ │ │ ├── TableBodyCell │ │ │ │ ├── TableBodyCell.jsx │ │ │ │ └── index.js │ │ │ ├── TableCell.module.scss │ │ │ └── TableHeaderCell │ │ │ │ ├── TableHeaderCell.jsx │ │ │ │ └── index.js │ │ ├── _settings.scss │ │ └── index.js │ ├── Tabs │ │ ├── README.md │ │ ├── Tabs.jsx │ │ ├── Tabs.module.scss │ │ ├── TabsItem.jsx │ │ ├── TabsItem.module.scss │ │ ├── __tests__ │ │ │ ├── Tabs.test.jsx │ │ │ └── TabsItem.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Text │ │ ├── README.md │ │ ├── Text.jsx │ │ ├── Text.module.scss │ │ ├── __tests__ │ │ │ └── Text.test.jsx │ │ ├── _helpers │ │ │ ├── getRootClampClassName.js │ │ │ ├── getRootHyphensClassName.js │ │ │ └── getRootWordWrappingClassName.js │ │ └── index.js │ ├── TextArea │ │ ├── README.md │ │ ├── TextArea.jsx │ │ ├── TextArea.module.scss │ │ ├── __tests__ │ │ │ └── TextArea.test.jsx │ │ └── index.js │ ├── TextField │ │ ├── README.md │ │ ├── TextField.jsx │ │ ├── TextField.module.scss │ │ ├── __tests__ │ │ │ └── TextField.test.jsx │ │ └── index.js │ ├── TextLink │ │ ├── README.md │ │ ├── TextLink.jsx │ │ ├── TextLink.module.scss │ │ ├── __tests__ │ │ │ └── Link.test.jsx │ │ ├── _theme.scss │ │ └── index.js │ ├── Toggle │ │ ├── README.md │ │ ├── Toggle.jsx │ │ ├── Toggle.module.scss │ │ ├── __tests__ │ │ │ └── Toggle.test.jsx │ │ └── index.js │ ├── Toolbar │ │ ├── README.md │ │ ├── Toolbar.jsx │ │ ├── Toolbar.module.scss │ │ ├── ToolbarGroup.jsx │ │ ├── ToolbarItem.jsx │ │ ├── __tests__ │ │ │ ├── Toolbar.test.jsx │ │ │ ├── ToolbarGroup.test.jsx │ │ │ └── ToolbarItem.test.jsx │ │ ├── _helpers │ │ │ ├── getAlignClassName.js │ │ │ └── getJustifyClassName.js │ │ ├── _theme.scss │ │ └── index.js │ └── _helpers │ │ ├── getRootColorClassName.js │ │ ├── getRootPriorityClassName.js │ │ ├── getRootSizeClassName.js │ │ ├── getRootValidationStateClassName.js │ │ ├── isChildrenEmpty.js │ │ └── resolveContextOrProp.js ├── docs │ ├── _assets │ │ ├── js │ │ │ ├── ruiIcon.js │ │ │ └── ruiSwatch.js │ │ ├── racom.svg │ │ └── stylesheets │ │ │ └── extra.css │ ├── _overrides │ │ └── main.html │ ├── contribute │ │ ├── api.md │ │ ├── composition.md │ │ ├── css.md │ │ ├── general-guidelines.md │ │ └── releasing.md │ ├── css-helpers │ │ ├── animation.md │ │ ├── box-alignment.md │ │ ├── colors.md │ │ ├── display.md │ │ ├── spacing.md │ │ └── typography.md │ ├── customize │ │ ├── font.md │ │ ├── global-props.md │ │ ├── theming │ │ │ ├── forms.md │ │ │ └── overview.md │ │ └── translations.md │ ├── foundation │ │ ├── accessibility.md │ │ ├── borders.md │ │ ├── breakpoints.md │ │ ├── collections.md │ │ ├── colors.md │ │ ├── design-tokens.md │ │ ├── icons.md │ │ ├── shadows.md │ │ ├── spacing.md │ │ └── typography.md │ └── getting-started │ │ ├── browsers-and-devices.md │ │ ├── installation.md │ │ └── usage.md ├── docsCustomProperties.js ├── foundation.scss ├── helpers.scss ├── helpers │ ├── classNames │ │ ├── README.md │ │ ├── __tests__ │ │ │ └── classNames.test.js │ │ ├── classNames.js │ │ └── index.js │ └── transferProps │ │ ├── README.md │ │ ├── __tests__ │ │ └── transferProps.test.js │ │ ├── index.js │ │ └── transferProps.js ├── index.js ├── index.md ├── index.scss ├── layers.scss ├── providers │ ├── globalProps │ │ ├── GlobalPropsContext.jsx │ │ ├── GlobalPropsProvider.jsx │ │ ├── __tests__ │ │ │ └── GlobalPropsProvider.test.jsx │ │ ├── index.js │ │ └── withGlobalProps.jsx │ └── translations │ │ ├── TranslationsContext.jsx │ │ ├── TranslationsProvider.jsx │ │ ├── __tests__ │ │ └── TranslationsProvider.test.jsx │ │ └── index.js ├── styles │ ├── _utilities.scss │ ├── elements │ │ ├── _code.scss │ │ ├── _links.scss │ │ ├── _lists.scss │ │ ├── _page.scss │ │ ├── _rulers.scss │ │ └── _small.scss │ ├── generic │ │ ├── _box-sizing.scss │ │ ├── _focus.scss │ │ ├── _forms.scss │ │ ├── _reset.scss │ │ └── _shared.scss │ ├── helpers │ │ └── _animation.scss │ ├── settings │ │ ├── _animations.scss │ │ ├── _breakpoints.scss │ │ ├── _collections.scss │ │ ├── _escaped-characters.scss │ │ ├── _form-fields.scss │ │ ├── _forms.scss │ │ └── _utilities.scss │ ├── theme-constants │ │ ├── _breakpoints.scss │ │ ├── _colors.scss │ │ └── _svg.scss │ ├── theme │ │ ├── _accessibility.scss │ │ ├── _borders.scss │ │ ├── _code.scss │ │ ├── _form-fields.scss │ │ ├── _links.scss │ │ ├── _lists.scss │ │ ├── _page.scss │ │ ├── _spacing.scss │ │ └── _typography.scss │ └── tools │ │ ├── _accessibility.scss │ │ ├── _breakpoint.scss │ │ ├── _caret.scss │ │ ├── _collections.scss │ │ ├── _colors.scss │ │ ├── _links.scss │ │ ├── _reset.scss │ │ ├── _scrollbar.scss │ │ ├── _spacing.scss │ │ ├── _string.scss │ │ ├── _svg.scss │ │ ├── _transition.scss │ │ ├── _utilities.scss │ │ └── form-fields │ │ ├── _box-field-elements.scss │ │ ├── _box-field-layout.scss │ │ ├── _box-field-sizes.scss │ │ ├── _foundation.scss │ │ ├── _inline-field-elements.scss │ │ ├── _inline-field-layout.scss │ │ └── _variants.scss ├── theme.scss ├── translations │ └── en.js └── utils │ ├── __tests__ │ └── mergeDeep.js │ └── mergeDeep.js ├── stylelint.config.js ├── tests ├── mocks │ └── svgrMock.jsx ├── propTests │ ├── actionColorPropTest.js │ ├── alignPropTest.js │ ├── blockPropTest.js │ ├── childrenEmptyPropTest.js │ ├── densePropTest.js │ ├── disabledPropTest.js │ ├── feedbackColorPropTest.js │ ├── fullWidthPropTest.js │ ├── helpTextPropTest.jsx │ ├── idPropTest.js │ ├── isLabelVisibleTest.js │ ├── justifyPropTest.js │ ├── labelPropTest.js │ ├── layoutPropTest.js │ ├── neutralColorPropTest.js │ ├── noWrapPropTest.js │ ├── raisedPropTest.js │ ├── refPropTest.js │ ├── renderAsRequiredPropTest.js │ ├── requiredPropTest.js │ ├── sizePropTest.js │ ├── tagPropTest.js │ ├── validationStatePropTest.js │ ├── validationTextPropTest.jsx │ └── variantPropTest.js ├── providerTests │ └── formLayoutProviderTest.jsx ├── setupJest.js └── setupTestingLibrary.js └── webpack.config.babel.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | # Blink/Chromium based 4 | last 1 ChromeAndroid versions 5 | last 2 Chrome versions 6 | last 2 Edge versions 7 | 8 | # Gecko based 9 | last 1 FirefoxAndroid versions 10 | last 2 Firefox versions 11 | Firefox ESR 12 | 13 | # WebKit based 14 | last 1 iOS major versions 15 | last 1 Safari major versions 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.{scss,md}] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # Host system port where the live documentation is to be made accessible 2 | COMPOSE_START_PORT=8000 3 | 4 | # Ownership of the files created in the container 5 | COMPOSE_UID=1000 6 | COMPOSE_GID=1000 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@visionappscz/eslint-config-visionapps" 4 | ], 5 | "env": { 6 | "browser": true, 7 | "jest": true 8 | }, 9 | "ignorePatterns": ["src/docs/_assets/generated"], 10 | "overrides": [ 11 | { 12 | "files": ["**/*.md"], 13 | "extends": [ 14 | "plugin:markdown/recommended" 15 | ] 16 | } 17 | ], 18 | "parser": "@babel/eslint-parser", 19 | "rules": { 20 | "import/prefer-default-export": "off", 21 | "no-console": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addAssignees: author 2 | runOnDraft: true 3 | -------------------------------------------------------------------------------- /.github/markdown-link-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^/components/" 5 | }, 6 | { 7 | "pattern": "^/docs/" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | BC: bc/* 2 | feature: [feature/*, task/*] 3 | fix: [fix/*, bug/*, bugfix/*] 4 | refactoring: [refactor/*, refactoring/*] 5 | documentation: [docs/*, documentation/*] 6 | maintenance: maintenance/* 7 | 'skip changelog': release/* 8 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'skip changelog' 5 | categories: 6 | - title: '⚠️ Breaking Changes' 7 | labels: 8 | - 'BC' 9 | - title: '🌟 Features' 10 | labels: 11 | - 'feature' 12 | - 'enhancement' 13 | - title: '🐞 Bug Fixes' 14 | labels: 15 | - 'fix' 16 | - 'bug' 17 | - 'bugfix' 18 | - title: '♻️ Refactoring' 19 | labels: 20 | - 'refactoring' 21 | - title: '📖 Documentation' 22 | labels: 23 | - 'documentation' 24 | - title: '🔧 Maintenance' 25 | labels: 26 | - 'maintenance' 27 | version-resolver: 28 | major: 29 | labels: 30 | - 'BC' 31 | minor: 32 | labels: 33 | - 'feature' 34 | patch: 35 | labels: 36 | - 'fix' 37 | - 'bug' 38 | - 'bugfix' 39 | default: patch 40 | change-template: '- $TITLE by @$AUTHOR (PR #$NUMBER)' 41 | template: | 42 | $CHANGES 43 | footer: | 44 | 45 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 46 | -------------------------------------------------------------------------------- /.github/workflows/broken-links-check.yml: -------------------------------------------------------------------------------- 1 | name: Broken Links Check 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 * * *' # daily at 6:00 UTC (7:00 CET, 8:00 CEST) 6 | 7 | jobs: 8 | broken_link_check: 9 | runs-on: ubuntu-24.04 10 | name: Check react-ui.io for broken links 11 | steps: 12 | - name: Check for broken links 13 | id: link-report 14 | uses: celinekurpershoek/link-checker@v1.0.2 15 | with: 16 | url: 'https://react-ui.io' 17 | honorRobotExclusions: false 18 | ignorePatterns: "*.racom.eu*" 19 | 20 | - name: Get the result 21 | run: echo "${{steps.link-report.outputs.result}}" 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build distribution CSS and JS 8 | runs-on: ubuntu-24.04 9 | strategy: 10 | matrix: 11 | node: [ 20, 22 ] 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | cache: npm 21 | 22 | - name: Print Node.js and npm version 23 | run: node --version && npm --version 24 | 25 | - name: Install 26 | run: npm ci 27 | 28 | - name: Build 29 | run: npm run build 30 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: [ pull_request ] 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | name: Build Docs 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.x 20 | 21 | - name: Get cache ID 22 | run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 23 | 24 | - name: Restore cache 25 | uses: actions/cache@v4 26 | with: 27 | key: mkdocs-material-${{ env.cache_id }} 28 | path: .cache 29 | restore-keys: | 30 | mkdocs-material- 31 | 32 | - name: Install MkDocs 33 | run: pip install 'mkdocs-material>=9.0.0,<10.0.0' 34 | 35 | - name: Build MkDocs 36 | run: mkdocs build 37 | -------------------------------------------------------------------------------- /.github/workflows/git.yml: -------------------------------------------------------------------------------- 1 | name: Git Checks 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | block-merge-with-autosquash-commits: 7 | name: Block merge with autosquash commits 8 | runs-on: ubuntu-24.04 9 | 10 | steps: 11 | - name: Block merge with autosquash commits 12 | uses: xt0rted/block-autosquash-commits-action@v2 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - name: Clone repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | cache: npm 18 | 19 | - name: Print Node.js and npm version 20 | run: node --version && npm --version 21 | 22 | - name: Install 23 | run: npm ci 24 | 25 | - name: Test 26 | run: npm run lint 27 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-meta.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Meta 2 | 3 | on: 4 | pull_request: 5 | types: [ opened ] 6 | 7 | jobs: 8 | process_pr_meta: 9 | name: Process PR meta 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Assign to author 13 | uses: kentaro-m/auto-assign-action@v2.0.0 # Specify also the minor version because v2 does not exist 14 | 15 | - name: Add labels 16 | uses: TimonVS/pr-labeler-action@v5 17 | env: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: '0 6 * * *' # daily at 6:00 UTC (7:00 CET, 8:00 CEST) 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | cache: npm 21 | 22 | - name: Print Node.js and npm version 23 | run: node --version && npm --version 24 | 25 | - name: Install 26 | run: npm ci 27 | 28 | - name: Test and generate code coverage info 29 | run: npm test 30 | 31 | - name: Upload code coverage info to Coveralls 32 | uses: coverallsapp/github-action@v2 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /site 5 | /src/docs/_assets/generated/* 6 | .env 7 | statistics.html 8 | !.gitkeep 9 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | // See https://github.com/DavidAnson/markdownlint#rules--aliases for the complete list of rules. 2 | 3 | { 4 | "default": true, 5 | "MD007": { // Unordered list indentation 6 | "indent": 4 // 4 spaces so parsers (MkDocs, Bitbucket, etc.) can render nested lists correctly 7 | }, 8 | "MD013": { // Line length 9 | "code_block_line_length": 120, // Max length inside code blocks 10 | "tables": false // Do not check length inside tables 11 | }, 12 | "MD024": { // Allow duplicate headings 13 | "allow_different_nesting": true // Allow same heading level under different parents 14 | }, 15 | "MD033": false // Allow inline HTML and custom components 16 | } 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /.idea 3 | /coverage 4 | /docker 5 | /node_modules 6 | /public 7 | /site 8 | /src/**/__tests__ 9 | /src/docs/ 10 | /src/docsCustomProperties.js 11 | /src/index.md 12 | /tests 13 | .browserslistrc 14 | .editorconfig 15 | .env 16 | .env.dist 17 | .eslintrc 18 | .gitignore 19 | .markdownlint.jsonc 20 | babel.config.js 21 | CODEOWNERS 22 | CONTRIBUTING.md 23 | docker-compose.yml 24 | jest.config.js 25 | mkdocs.yml 26 | package-lock.json 27 | postcss.config.js 28 | RELEASING.md 29 | statistics.html 30 | stylelint.config.js 31 | webpack.config.babel.js 32 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners for code review 2 | * @developers 3 | 4 | # Following code owners are temporarily disabled due to unavailability to merge PRs 5 | # @see https://github.com/react-ui-org/react-ui/issues/632 6 | 7 | # Default owners for file types 8 | # *.js @mbohal @bedrich-schindler @developers 9 | # *.jsx @mbohal @bedrich-schindler @developers 10 | # *.md @adamkudrna @developers 11 | # *.scss @adamkudrna @developers 12 | 13 | # Default owners for directories 14 | # /docker @mbohal @bedrich-schindler @developers 15 | # /src/docs @adamkudrna @developers 16 | 17 | # Default owners for specific files 18 | # /.browserslistrc @adamkudrna @developers 19 | # /.eslintrc @mbohal @bedrich-schindler @developers 20 | # /.markdownlint.jsonc @adamkudrna @developers 21 | # /babel.config.js @mbohal @bedrich-schindler @developers 22 | # /docker-compose.yml @mbohal @bedrich-schindler @developers 23 | # /jest.config.js @mbohal @bedrich-schindler @developers 24 | # /postcss.config.js @adamkudrna @developers 25 | # /stylelint.config.js @adamkudrna @developers 26 | # /webpack.config.babel.js @mbohal @bedrich-schindler @developers 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 @react-ui-org 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@babel/plugin-transform-modules-commonjs', 4 | '@babel/plugin-proposal-class-properties', 5 | ], 6 | presets: [ 7 | [ 8 | '@babel/preset-env', 9 | { 10 | corejs: 3, 11 | useBuiltIns: 'usage', 12 | }, 13 | ], 14 | '@babel/preset-react', 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Base services - do not run directly 3 | mkdocs: 4 | build: docker/mkdocs 5 | user: ${COMPOSE_UID-1000}:${COMPOSE_GID-1000} 6 | volumes: 7 | - .:/workspace:z 8 | node: 9 | build: docker/node 10 | user: ${COMPOSE_UID-1000}:${COMPOSE_GID-1000} 11 | volumes: 12 | - .:/workspace:z 13 | 14 | # Dev services 15 | mkdocs_dev_server: 16 | extends: mkdocs 17 | entrypoint: mkdocs serve 18 | ports: 19 | - ${COMPOSE_START_PORT-8000}:8000 20 | node_dev_server: 21 | extends: node 22 | entrypoint: npm start 23 | node_shell: 24 | extends: node 25 | entrypoint: bash 26 | 27 | # Build services 28 | mkdocs_build_site: 29 | extends: mkdocs 30 | entrypoint: mkdocs build 31 | node_build_site: 32 | extends: node 33 | entrypoint: npm run build 34 | -------------------------------------------------------------------------------- /docker/mkdocs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM squidfunk/mkdocs-material:9 2 | RUN mkdir /workspace 3 | WORKDIR /workspace 4 | -------------------------------------------------------------------------------- /docker/node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | RUN mkdir /workspace 3 | WORKDIR /workspace 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | ], 6 | moduleNameMapper: { 7 | '\\.scss$': 'identity-obj-proxy', 8 | '\\.svg$': '/tests/mocks/svgrMock.jsx', 9 | }, 10 | setupFiles: [ 11 | '/tests/setupJest.js', 12 | ], 13 | setupFilesAfterEnv: [ 14 | '/tests/setupTestingLibrary.js', 15 | ], 16 | testEnvironment: '@happy-dom/jest-environment', 17 | transformIgnorePatterns: [ 18 | 'node_modules/(?!(@react-ui-org))', 19 | ], 20 | verbose: true, 21 | }; 22 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | autoprefixer({ 6 | grid: true, 7 | }), 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /public/racom.svg: -------------------------------------------------------------------------------- 1 | 2 | RACOM 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/CNAME: -------------------------------------------------------------------------------- 1 | react-ui.io 2 | -------------------------------------------------------------------------------- /src/components/Alert/__tests__/Alert.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | screen, 5 | within, 6 | } from '@testing-library/react'; 7 | import userEvent from '@testing-library/user-event'; 8 | import { feedbackColorPropTest } from '../../../../tests/propTests/feedbackColorPropTest'; 9 | import defaultTranslations from '../../../translations/en'; 10 | import { Alert } from '../Alert'; 11 | 12 | const mandatoryProps = { 13 | children: 'content', 14 | translations: defaultTranslations.Alert, 15 | }; 16 | 17 | describe('rendering', () => { 18 | it.each([ 19 | [ 20 | { children:
content text
}, 21 | (rootElement) => expect(within(rootElement).getByText('content text')), 22 | ], 23 | ...feedbackColorPropTest, 24 | [ 25 | { icon: (
icon
) }, 26 | (rootElement) => expect(within(rootElement).getByText('icon')), 27 | ], 28 | [ 29 | { 30 | id: 'id', 31 | onClose: () => {}, 32 | }, 33 | (rootElement) => { 34 | expect(rootElement).toHaveAttribute('id', 'id'); 35 | expect(within(rootElement).getByText('content')).toHaveAttribute('id', 'id__content'); 36 | expect(within(rootElement).getByTitle('Close')).toHaveAttribute('id', 'id__close'); 37 | }, 38 | ], 39 | ])('renders with props: "%s"', (testedProps, assert) => { 40 | const dom = render(( 41 | 45 | )); 46 | 47 | assert(dom.container.firstChild); 48 | }); 49 | }); 50 | 51 | describe('functionality', () => { 52 | it('calls onClose() on Close button click', async () => { 53 | const spy = jest.fn(); 54 | render(( 55 | 59 | )); 60 | 61 | await userEvent.click(screen.getByTitle('Close')); 62 | expect(spy).toHaveBeenCalled(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/Alert/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../styles/settings/collections"; 3 | @use "../../styles/theme/typography"; 4 | @use "theme"; 5 | 6 | $font-size: map.get(typography.$font-size-values, 1); 7 | $line-height: typography.$line-height-base; 8 | $min-height: calc(#{$font-size} * #{$line-height} + 2 * #{theme.$padding}); 9 | 10 | $colors: collections.$feedback-colors; 11 | $themeable-properties: color, foreground-color, background-color; 12 | -------------------------------------------------------------------------------- /src/components/Alert/_theme.scss: -------------------------------------------------------------------------------- 1 | $padding: var(--rui-Alert__padding); 2 | $font-weight: var(--rui-Alert__font-weight); 3 | $border-width: var(--rui-Alert__border-width); 4 | $border-radius: var(--rui-Alert__border-radius); 5 | $emphasis-font-weight: var(--rui-Alert__emphasis__font-weight); 6 | $stripe-width: var(--rui-Alert__stripe__width); 7 | -------------------------------------------------------------------------------- /src/components/Alert/index.js: -------------------------------------------------------------------------------- 1 | export { default as Alert } from './Alert'; 2 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { getRootColorClassName } from '../_helpers/getRootColorClassName'; 7 | import { getRootPriorityClassName } from '../_helpers/getRootPriorityClassName'; 8 | import styles from './Badge.module.scss'; 9 | 10 | export const Badge = ({ 11 | color, 12 | label, 13 | priority, 14 | ...restProps 15 | }) => ( 16 |
24 | {label} 25 |
26 | ); 27 | 28 | Badge.defaultProps = { 29 | color: 'note', 30 | priority: 'filled', 31 | }; 32 | 33 | Badge.propTypes = { 34 | /** 35 | * Color to clarify importance and meaning of the badge. Implements 36 | * [Feedback and Neutral color collections](/docs/foundation/collections#colors). 37 | */ 38 | color: PropTypes.oneOf(['success', 'warning', 'danger', 'help', 'info', 'note', 'light', 'dark']), 39 | /** 40 | * Text to be displayed. 41 | */ 42 | label: PropTypes.string.isRequired, 43 | /** 44 | * Visual priority to highlight or suppress the badge. 45 | */ 46 | priority: PropTypes.oneOf(['filled', 'outline']), 47 | }; 48 | 49 | export const BadgeWithGlobalProps = withGlobalProps(Badge, 'Badge'); 50 | 51 | export default BadgeWithGlobalProps; 52 | -------------------------------------------------------------------------------- /src/components/Badge/__tests__/Badge.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { feedbackColorPropTest } from '../../../../tests/propTests/feedbackColorPropTest'; 7 | import { neutralColorPropTest } from '../../../../tests/propTests/neutralColorPropTest'; 8 | import { Badge } from '../Badge'; 9 | 10 | const mandatoryProps = { 11 | label: 'label', 12 | }; 13 | 14 | describe('rendering', () => { 15 | it.each([ 16 | ...feedbackColorPropTest, 17 | ...neutralColorPropTest, 18 | [ 19 | { label: 'label text' }, 20 | (rootElement) => expect(within(rootElement).getByText('label text')), 21 | ], 22 | [ 23 | { priority: 'filled' }, 24 | (rootElement) => expect(rootElement).toHaveClass('isRootPriorityFilled'), 25 | ], 26 | [ 27 | { priority: 'outline' }, 28 | (rootElement) => expect(rootElement).toHaveClass('isRootPriorityOutline'), 29 | ], 30 | ])('renders with props: "%s"', (testedProps, assert) => { 31 | const dom = render(( 32 | 36 | )); 37 | 38 | assert(dom.container.firstChild); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/Badge/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:list"; 2 | @use "../../styles/settings/collections"; 3 | 4 | $badge-size: 1.25rem; 5 | 6 | $colors: list.join(collections.$feedback-colors, collections.$neutral-colors); 7 | $themeable-properties-filled: color, background-color; 8 | $themeable-properties-outline: color; 9 | -------------------------------------------------------------------------------- /src/components/Badge/index.js: -------------------------------------------------------------------------------- 1 | export { default as Badge } from './Badge'; 2 | -------------------------------------------------------------------------------- /src/components/Button/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:list"; 2 | @use "../../styles/settings/collections"; 3 | @use "../../styles/theme/typography"; 4 | @use "../../styles/tools/spacing"; 5 | 6 | $font-family: typography.$font-family-base; 7 | $line-height: typography.$line-height-small; 8 | $icon-spacing: spacing.of(2); 9 | 10 | $group-z-indexes: ( 11 | button: auto, 12 | button-hover: 1, 13 | separator: 2, 14 | button-overflowing-elements: 3, 15 | ); 16 | 17 | $colors: 18 | list.join( 19 | list.join(collections.$action-colors, collections.$feedback-colors), 20 | collections.$neutral-colors 21 | ); 22 | $themeable-properties: ( 23 | filled: (color, border-color, background, box-shadow), 24 | outline: (color, border-color, background), 25 | flat: (color, background), 26 | ); 27 | -------------------------------------------------------------------------------- /src/components/Button/_theme.scss: -------------------------------------------------------------------------------- 1 | $font-weight: var(--rui-Button__font-weight); 2 | $letter-spacing: var(--rui-Button__letter-spacing); 3 | $text-transform: var(--rui-Button__text-transform); 4 | $border-width: var(--rui-Button__border-width); 5 | $border-radius: var(--rui-Button__border-radius); 6 | 7 | $disabled-opacity: var(--rui-Button--disabled__opacity); 8 | $disabled-cursor: var(--rui-Button--disabled__cursor); 9 | $feedback-opacity: var(--rui-Button--feedback__opacity); 10 | $feedback-cursor: var(--rui-Button--feedback__cursor); 11 | 12 | $sizes: ( 13 | small: ( 14 | height: var(--rui-Button--small__height), 15 | padding-y: var(--rui-Button--small__padding-y), 16 | padding-x: var(--rui-Button--small__padding-x), 17 | font-size: var(--rui-Button--small__font-size), 18 | ), 19 | medium: ( 20 | height: var(--rui-Button--medium__height), 21 | padding-y: var(--rui-Button--medium__padding-y), 22 | padding-x: var(--rui-Button--medium__padding-x), 23 | font-size: var(--rui-Button--medium__font-size), 24 | ), 25 | large: ( 26 | height: var(--rui-Button--large__height), 27 | padding-y: var(--rui-Button--large__padding-y), 28 | padding-x: var(--rui-Button--large__padding-x), 29 | font-size: var(--rui-Button--large__font-size), 30 | ), 31 | ); 32 | -------------------------------------------------------------------------------- /src/components/Button/helpers/getRootLabelVisibilityClassName.js: -------------------------------------------------------------------------------- 1 | export default (labelVisibility, styles) => { 2 | // Intentionally omitting `xs` which means label is visible on all screen sizes. 3 | 4 | if (labelVisibility === 'sm') { 5 | return styles.hasLabelVisibleSm; 6 | } 7 | 8 | if (labelVisibility === 'md') { 9 | return styles.hasLabelVisibleMd; 10 | } 11 | 12 | if (labelVisibility === 'lg') { 13 | return styles.hasLabelVisibleLg; 14 | } 15 | 16 | if (labelVisibility === 'xl') { 17 | return styles.hasLabelVisibleXl; 18 | } 19 | 20 | if (labelVisibility === 'x2l') { 21 | return styles.hasLabelVisibleX2l; 22 | } 23 | 24 | if (labelVisibility === 'x3l') { 25 | return styles.hasLabelVisibleX3l; 26 | } 27 | 28 | if (labelVisibility === 'none') { 29 | return styles.hasLabelHidden; 30 | } 31 | 32 | return null; 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/Button/helpers/getRootPriorityClassName.js: -------------------------------------------------------------------------------- 1 | export default (priority, styles) => { 2 | if (priority === 'filled') { 3 | return styles.isRootPriorityFilled; 4 | } 5 | 6 | if (priority === 'outline') { 7 | return styles.isRootPriorityOutline; 8 | } 9 | 10 | if (priority === 'flat') { 11 | return styles.isRootPriorityFlat; 12 | } 13 | 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/ButtonGroup.module.scss: -------------------------------------------------------------------------------- 1 | // 1. ButtonGroup gap is implemented using the `margin` property on buttons so the buttons can overlap and reduce 2 | // duplicate borders. 3 | 4 | @use "theme"; 5 | 6 | @layer components.button-group { 7 | .root { 8 | --rui-local-inner-border-radius: #{theme.$inner-border-radius}; 9 | 10 | display: inline-flex; // 1. 11 | } 12 | 13 | .isRootPriorityFilled { 14 | --rui-local-gap: #{theme.$filled-gap}; 15 | --rui-local-separator-width: #{theme.$filled-separator-width}; 16 | --rui-local-separator-color: #{theme.$filled-separator-color}; 17 | } 18 | 19 | .isRootPriorityOutline { 20 | --rui-local-gap: #{theme.$outline-gap}; 21 | --rui-local-separator-width: #{theme.$outline-separator-width}; 22 | --rui-local-separator-color: #{theme.$outline-separator-color}; 23 | } 24 | 25 | .isRootPriorityFlat { 26 | --rui-local-gap: #{theme.$flat-gap}; 27 | --rui-local-separator-width: #{theme.$flat-separator-width}; 28 | --rui-local-separator-color: #{theme.$flat-separator-color}; 29 | } 30 | 31 | .isRootBlock { 32 | display: flex; 33 | width: 100%; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/ButtonGroupContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ButtonGroupContext = React.createContext(null); 4 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/_theme.scss: -------------------------------------------------------------------------------- 1 | $inner-border-radius: var(--rui-ButtonGroup__inner-border-radius); 2 | 3 | $filled-gap: var(--rui-ButtonGroup--filled__gap); 4 | $filled-separator-width: var(--rui-ButtonGroup--filled__separator__width); 5 | $filled-separator-color: var(--rui-ButtonGroup--filled__separator__color); 6 | 7 | $outline-gap: var(--rui-ButtonGroup--outline__gap); 8 | $outline-separator-width: var(--rui-ButtonGroup--outline__separator__width); 9 | $outline-separator-color: var(--rui-ButtonGroup--outline__separator__color); 10 | 11 | $flat-gap: var(--rui-ButtonGroup--flat__gap); 12 | $flat-separator-width: var(--rui-ButtonGroup--flat__separator__width); 13 | $flat-separator-color: var(--rui-ButtonGroup--flat__separator__color); 14 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/index.js: -------------------------------------------------------------------------------- 1 | export { default as ButtonGroup } from './ButtonGroup'; 2 | export { ButtonGroupContext } from './ButtonGroupContext'; 3 | -------------------------------------------------------------------------------- /src/components/Card/Card.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { getRootColorClassName } from '../_helpers/getRootColorClassName'; 7 | import styles from './Card.module.scss'; 8 | 9 | export const Card = ({ 10 | children, 11 | dense, 12 | disabled, 13 | raised, 14 | color, 15 | ...restProps 16 | }) => ( 17 |
27 | {children} 28 |
29 | ); 30 | 31 | Card.defaultProps = { 32 | color: undefined, 33 | dense: false, 34 | disabled: false, 35 | raised: false, 36 | }; 37 | 38 | Card.propTypes = { 39 | /** 40 | * Slot for individual card elements that build up the inner layout: 41 | * * `CardBody` 42 | * * `CardFooter` 43 | * * `ScrollView` 44 | */ 45 | children: PropTypes.node.isRequired, 46 | /** 47 | * Color to clarify importance and meaning of the card. Implements 48 | * [Feedback color collection](/docs/foundation/collections#colors). 49 | */ 50 | color: PropTypes.oneOf(['success', 'warning', 'danger', 'help', 'info', 'note']), 51 | /** 52 | * Make the card more compact. 53 | */ 54 | dense: PropTypes.bool, 55 | /** 56 | * If `true`, the card will be disabled. 57 | */ 58 | disabled: PropTypes.bool, 59 | /** 60 | * Add shadow to pull the card above surface. 61 | */ 62 | raised: PropTypes.bool, 63 | }; 64 | 65 | export const CardWithGlobalProps = withGlobalProps(Card, 'Card'); 66 | 67 | export default CardWithGlobalProps; 68 | -------------------------------------------------------------------------------- /src/components/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Retain equal card widths in flex and grid layouts independently on their content. 2 | 3 | @use "../../styles/tools/collections"; 4 | @use "settings"; 5 | @use "theme"; 6 | 7 | @layer components.card { 8 | .root { 9 | --rui-local-padding: #{theme.$padding}; 10 | 11 | display: flex; 12 | flex-direction: column; 13 | min-width: 0; // 1. 14 | color: var(--rui-local-color); 15 | border: theme.$border-width solid var(--rui-local-border-color, theme.$border-color); 16 | border-radius: theme.$border-radius; 17 | background-color: var(--rui-local-background-color, theme.$background-color); 18 | } 19 | 20 | .body { 21 | flex: 1 0 auto; 22 | padding: var(--rui-local-padding); 23 | } 24 | 25 | .footer { 26 | padding: var(--rui-local-padding); 27 | } 28 | 29 | .isRootDense { 30 | --rui-local-padding: #{theme.$dense-padding}; 31 | } 32 | 33 | .isRootRaised { 34 | box-shadow: theme.$raised-box-shadow; 35 | } 36 | 37 | .isRootDisabled { 38 | background-color: theme.$disabled-background-color; 39 | opacity: theme.$disabled-opacity; 40 | } 41 | 42 | @each $color in settings.$colors { 43 | @include collections.generate-class( 44 | $prefix: "rui-", 45 | $component-name: "Card", 46 | $variant-name: "color", 47 | $variant-value: $color, 48 | $inherit-link-color: true, 49 | $properties: settings.$themeable-properties, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Card/CardBody.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import styles from './Card.module.scss'; 6 | 7 | export const CardBody = ({ 8 | children, 9 | ...restProps 10 | }) => ( 11 |
15 | {children} 16 |
17 | ); 18 | 19 | CardBody.propTypes = { 20 | /** 21 | * Content of the card. 22 | */ 23 | children: PropTypes.node.isRequired, 24 | }; 25 | 26 | export const CardBodyWithGlobalProps = withGlobalProps(CardBody, 'CardBody'); 27 | 28 | export default CardBodyWithGlobalProps; 29 | -------------------------------------------------------------------------------- /src/components/Card/CardFooter.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { transferProps } from '../../helpers/transferProps'; 4 | import { withGlobalProps } from '../../providers/globalProps'; 5 | import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; 6 | import styles from './Card.module.scss'; 7 | 8 | export const CardFooter = ({ 9 | children, 10 | ...restProps 11 | }) => { 12 | if (isChildrenEmpty(children)) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
21 | {children} 22 |
23 | ); 24 | }; 25 | 26 | CardFooter.defaultProps = { 27 | children: null, 28 | }; 29 | 30 | CardFooter.propTypes = { 31 | /** 32 | * Card actions, usually buttons. 33 | */ 34 | children: PropTypes.node, 35 | }; 36 | 37 | export const CardFooterWithGlobalProps = withGlobalProps(CardFooter, 'CardFooter'); 38 | 39 | export default CardFooterWithGlobalProps; 40 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/Card.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { feedbackColorPropTest } from '../../../../tests/propTests/feedbackColorPropTest'; 7 | import { raisedPropTest } from '../../../../tests/propTests/raisedPropTest'; 8 | import { ScrollView } from '../../ScrollView'; 9 | import { Card } from '../Card'; 10 | import { CardBody } from '../CardBody'; 11 | import { CardFooter } from '../CardFooter'; 12 | import { densePropTest } from '../../../../tests/propTests/densePropTest'; 13 | 14 | const mandatoryProps = { 15 | children: card body content, 16 | }; 17 | 18 | describe('rendering', () => { 19 | it.each([ 20 | [ 21 | { 22 | children: [ 23 | card body content, 24 | card footer content, 25 | ], 26 | }, 27 | (rootElement) => { 28 | expect(within(rootElement).getByText('card body content')); 29 | expect(within(rootElement).getByText('card footer content')); 30 | }, 31 | ], 32 | [ 33 | { children: scroll view content }, 34 | (rootElement) => expect(within(rootElement).getByText('scroll view content')), 35 | ], 36 | ...feedbackColorPropTest, 37 | ...densePropTest('Root'), 38 | [ 39 | { disabled: true }, 40 | (rootElement) => expect(rootElement).toHaveClass('isRootDisabled'), 41 | ], 42 | [ 43 | { disabled: false }, 44 | (rootElement) => expect(rootElement).not.toHaveClass('isRootDisabled'), 45 | ], 46 | ...raisedPropTest, 47 | ])('renders with props: "%s"', (testedProps, assert) => { 48 | const dom = render(( 49 | 53 | )); 54 | 55 | assert(dom.container.firstChild); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/CardBody.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { CardBody } from '../CardBody'; 7 | 8 | const defaultProps = { 9 | children: 'content', 10 | }; 11 | 12 | describe('rendering', () => { 13 | it.each([ 14 | [ 15 | { children:
content text
}, 16 | (rootElement) => expect(within(rootElement).getByText('content text')), 17 | ], 18 | ])('renders with props: "%s"', (testedProps, assert) => { 19 | const dom = render(( 20 | 24 | )); 25 | 26 | assert(dom.container.firstChild); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/CardFooter.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { childrenEmptyPropTest } from '../../../../tests/propTests/childrenEmptyPropTest'; 7 | import { CardFooter } from '../CardFooter'; 8 | 9 | const defaultProps = { 10 | children: 'content', 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | [ 16 | { children:
content text
}, 17 | (rootElement) => expect(within(rootElement).getByText('content text')), 18 | ], 19 | ...childrenEmptyPropTest, 20 | ])('renders with props: "%s"', (testedProps, assert) => { 21 | const dom = render(( 22 | 26 | )); 27 | 28 | assert(dom.container.firstChild); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Card/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/settings/collections"; 2 | 3 | $colors: collections.$feedback-colors; 4 | $themeable-properties: color, border-color, background-color; 5 | -------------------------------------------------------------------------------- /src/components/Card/_theme.scss: -------------------------------------------------------------------------------- 1 | $padding: var(--rui-Card__padding); 2 | $border-width: var(--rui-Card__border-width); 3 | $border-color: var(--rui-Card__border-color); 4 | $border-radius: var(--rui-Card__border-radius); 5 | $background-color: var(--rui-Card__background-color); 6 | 7 | $dense-padding: var(--rui-Card--dense__padding); 8 | $raised-box-shadow: var(--rui-Card--raised__box-shadow); 9 | 10 | $disabled-background-color: var(--rui-Card--disabled__background-color); 11 | $disabled-opacity: var(--rui-Card--disabled__opacity); 12 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card'; 2 | export { default as CardBody } from './CardBody'; 3 | export { default as CardFooter } from './CardFooter'; 4 | -------------------------------------------------------------------------------- /src/components/CheckboxField/CheckboxField.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/tools/form-fields/foundation"; 2 | @use "../../styles/tools/form-fields/inline-field-elements"; 3 | @use "../../styles/tools/form-fields/inline-field-layout"; 4 | @use "../../styles/tools/form-fields/variants"; 5 | @use "../../styles/tools/accessibility"; 6 | 7 | @layer components.checkbox-field { 8 | // Foundation 9 | .root { 10 | @include foundation.root(); 11 | @include inline-field-layout.root(); 12 | @include inline-field-elements.min-tap-target($type: checkbox); 13 | @include variants.visual(check); 14 | } 15 | 16 | .label { 17 | @include foundation.label(); 18 | } 19 | 20 | .field { 21 | @include inline-field-layout.field($type: checkbox); 22 | } 23 | 24 | .input { 25 | @include inline-field-elements.check-input($type: checkbox); 26 | } 27 | 28 | .helpText, 29 | .validationText { 30 | @include foundation.help-text(); 31 | } 32 | 33 | .isRootRequired .label { 34 | @include foundation.label-required(); 35 | } 36 | 37 | // States 38 | .isRootStateInvalid { 39 | @include variants.validation(invalid); 40 | } 41 | 42 | .isRootStateValid { 43 | @include variants.validation(valid); 44 | } 45 | 46 | .isRootStateWarning { 47 | @include variants.validation(warning); 48 | } 49 | 50 | // Invisible label 51 | .isLabelHidden { 52 | @include accessibility.hide-text(); 53 | } 54 | 55 | // Layouts 56 | .hasRootLabelBefore { 57 | @include inline-field-layout.has-label-before(); 58 | } 59 | 60 | .isRootInFormLayout { 61 | @include inline-field-layout.in-form-layout(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/CheckboxField/index.js: -------------------------------------------------------------------------------- 1 | export { default as CheckboxField } from './CheckboxField'; 2 | -------------------------------------------------------------------------------- /src/components/FileInputField/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/theme/typography"; 2 | 3 | $drop-zone-color: var(--rui-color-text-primary); 4 | $drop-zone-disabled-color: var(--rui-color-text-primary-disabled); 5 | $drop-zone-border-color: var(--rui-color-border-primary); 6 | $drop-zone-hover-border-color: var(--rui-color-border-primary-hover); 7 | $drop-zone-active-border-color: var(--rui-color-border-primary-active); 8 | $drop-zone-dragging-border-color: var(--rui-color-border-primary-active); 9 | $drop-zone-disabled-border-color: var(--rui-color-border-primary); 10 | $drop-zone-background-color: var(--rui-color-background-basic); 11 | $drop-zone-disabled-background-color: var(--rui-color-background-disabled); 12 | $drop-zone-disabled-cursor: var(--rui-cursor-not-allowed); 13 | $drop-zone-font-weight: typography.$font-weight-base; 14 | $drop-zone-line-height: typography.$line-height-base; 15 | $drop-zone-font-family: typography.$font-family-base; 16 | -------------------------------------------------------------------------------- /src/components/FileInputField/index.js: -------------------------------------------------------------------------------- 1 | export { default as FileInputField } from './FileInputField'; 2 | -------------------------------------------------------------------------------- /src/components/FormLayout/FormLayoutContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const FormLayoutContext = React.createContext(null); 4 | -------------------------------------------------------------------------------- /src/components/FormLayout/FormLayoutCustomField.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/tools/form-fields/foundation"; 2 | @use "../../styles/tools/form-fields/box-field-layout"; 3 | @use "../../styles/tools/form-fields/box-field-sizes"; 4 | @use "../../styles/tools/form-fields/variants"; 5 | 6 | @layer components.form-layout { 7 | // Foundation 8 | .root { 9 | @include box-field-layout.in-form-layout(); 10 | @include variants.visual(custom); 11 | } 12 | 13 | .label { 14 | @include foundation.label(); 15 | } 16 | 17 | .isRootRequired .label { 18 | @include foundation.label-required(); 19 | } 20 | 21 | // States 22 | .isRootStateInvalid { 23 | @include variants.validation(invalid); 24 | } 25 | 26 | .isRootStateValid { 27 | @include variants.validation(valid); 28 | } 29 | 30 | .isRootStateWarning { 31 | @include variants.validation(warning); 32 | } 33 | 34 | // Layouts 35 | .isRootLayoutVertical, 36 | .isRootLayoutHorizontal { 37 | @include box-field-layout.vertical(); 38 | } 39 | 40 | .isRootLayoutHorizontal { 41 | @include box-field-layout.horizontal(); 42 | } 43 | 44 | .isRootLayoutVertical .field, 45 | .isRootLayoutHorizontal .field { 46 | width: auto; 47 | } 48 | 49 | .isRootFullWidth .field { 50 | justify-self: stretch; 51 | } 52 | 53 | // Sizes 54 | .isRootSizeSmall { 55 | @include box-field-sizes.size(small); 56 | } 57 | 58 | .isRootSizeMedium { 59 | @include box-field-sizes.size(medium); 60 | } 61 | 62 | .isRootSizeLarge { 63 | @include box-field-sizes.size(large); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/FormLayout/_theme.scss: -------------------------------------------------------------------------------- 1 | $row-gap: var(--rui-FormLayout__row-gap); 2 | $horizontal-label-width: var(--rui-FormLayout--horizontal__label__width); 3 | $horizontal-label-width-auto: var(--rui-FormLayout--horizontal__label__width--auto); 4 | $horizontal-label-width-limited: var(--rui-FormLayout--horizontal__label__width--limited); 5 | -------------------------------------------------------------------------------- /src/components/FormLayout/index.js: -------------------------------------------------------------------------------- 1 | export { default as FormLayout } from './FormLayout'; 2 | export { FormLayoutContext } from './FormLayoutContext'; 3 | export { default as FormLayoutCustomField } from './FormLayoutCustomField'; 4 | -------------------------------------------------------------------------------- /src/components/Grid/_helpers/__tests__/generateResponsiveCustomProperties.test.js: -------------------------------------------------------------------------------- 1 | import { generateResponsiveCustomProperties } from '../generateResponsiveCustomProperties'; 2 | 3 | describe('generateResponsiveCustomProperties', () => { 4 | test('with prop that is undefined', () => { 5 | expect( 6 | generateResponsiveCustomProperties(undefined, null), 7 | ).toEqual(null); 8 | }); 9 | 10 | test('with prop that is a spacing value', () => { 11 | expect( 12 | generateResponsiveCustomProperties(3, 'columns', 'spacing'), 13 | ).toEqual({ '--rui-local-columns-xs': 'var(--rui-dimension-space-3)' }); 14 | }); 15 | 16 | test('with prop that is not an object', () => { 17 | expect( 18 | generateResponsiveCustomProperties('1fr 1fr', 'columns'), 19 | ).toEqual({ '--rui-local-columns-xs': '1fr 1fr' }); 20 | }); 21 | 22 | test('with prop that is an object', () => { 23 | expect( 24 | generateResponsiveCustomProperties({ 25 | xs: '1fr', 26 | md: '1fr 2fr', /* eslint-disable-line sort-keys */ 27 | xl: '1fr 2fr 1fr', 28 | }, 'columns'), 29 | ).toEqual({ 30 | '--rui-local-columns-xs': '1fr', 31 | '--rui-local-columns-md': '1fr 2fr', /* eslint-disable-line sort-keys */ 32 | '--rui-local-columns-xl': '1fr 2fr 1fr', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/Grid/_helpers/generateResponsiveCustomProperties.js: -------------------------------------------------------------------------------- 1 | const prepareValueByType = (value, type) => { 2 | if (type === 'spacing') { 3 | return `var(--rui-dimension-space-${value})`; 4 | } 5 | 6 | return value; 7 | }; 8 | 9 | export const generateResponsiveCustomProperties = (prop, infix, type = null) => { 10 | if (typeof prop === 'undefined') { 11 | return null; 12 | } 13 | 14 | if (typeof prop !== 'object') { 15 | return { [`--rui-local-${infix}-xs`]: prepareValueByType(prop, type) }; 16 | } 17 | 18 | return Object.keys(prop).reduce((acc, breakpoint) => ({ 19 | ...acc, 20 | [`--rui-local-${infix}-${breakpoint}`]: prepareValueByType(prop[breakpoint], type), 21 | }), {}); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Grid/_settings.scss: -------------------------------------------------------------------------------- 1 | $grid-responsive-properties: [ 2 | columns, 3 | column-gap, 4 | rows, 5 | row-gap, 6 | auto-flow, 7 | align-content, 8 | align-items, 9 | justify-content, 10 | justify-items, 11 | ]; // stylelint-disable-line @stylistic/indentation -- Broken rule? 12 | 13 | $grid-span-responsive-properties: [ 14 | column-span, 15 | row-span, 16 | ]; // stylelint-disable-line @stylistic/indentation -- Broken rule? 17 | 18 | $initial-fallback-value: initial; 19 | -------------------------------------------------------------------------------- /src/components/Grid/_tools.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../styles/settings/breakpoints"; 3 | @use "../../styles/tools/breakpoint"; 4 | @use "settings"; 5 | 6 | // Generate fallback cascade using `var()` function fallbacks. 7 | // 8 | // $property-name: Custom property name 9 | // $current-breakpoint: Generate cascade for breakpoints smaller than this one 10 | @function _generate-custom-property-fallback-cascade($property-name, $current-breakpoint) { 11 | $fallback-cascade: settings.$initial-fallback-value; 12 | 13 | @each $breakpoint in map.keys(breakpoints.$values) { 14 | @if $breakpoint == $current-breakpoint { 15 | @return $fallback-cascade; 16 | } 17 | 18 | $fallback-cascade: var(--rui-local-#{$property-name}-#{$breakpoint}, $fallback-cascade); 19 | } 20 | } 21 | 22 | // Read custom properties within a given breakpoint and assign them to expected output custom 23 | // properties. Use a generated fallback cascade should the custom property be undefined. 24 | // 25 | // $properties: : > pairs> 26 | @mixin assign-responsive-custom-properties($properties) { 27 | @each $breakpoint in map.keys(breakpoints.$values) { 28 | @include breakpoint.up($breakpoint) { 29 | @each $property-name in $properties { 30 | --rui-local-#{$property-name}: 31 | var( 32 | --rui-local-#{$property-name}-#{$breakpoint}, 33 | #{_generate-custom-property-fallback-cascade($property-name, $breakpoint)} 34 | ); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Grid/index.js: -------------------------------------------------------------------------------- 1 | export { default as Grid } from './Grid'; 2 | export { default as GridSpan } from './GridSpan'; 3 | -------------------------------------------------------------------------------- /src/components/InputGroup/InputGroupContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const InputGroupContext = React.createContext(null); 4 | -------------------------------------------------------------------------------- /src/components/InputGroup/_theme.scss: -------------------------------------------------------------------------------- 1 | $gap: var(--rui-InputGroup__gap); 2 | $inner-border-radius: var(--rui-InputGroup__inner-border-radius); 3 | -------------------------------------------------------------------------------- /src/components/InputGroup/index.js: -------------------------------------------------------------------------------- 1 | export { default as InputGroup } from './InputGroup'; 2 | export { InputGroupContext } from './InputGroupContext'; 3 | -------------------------------------------------------------------------------- /src/components/Modal/ModalBody.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; 7 | import { getScrollingClassName } from './_helpers/getScrollingClassName'; 8 | import styles from './ModalBody.module.scss'; 9 | 10 | export const ModalBody = ({ 11 | children, 12 | scrolling, 13 | ...restProps 14 | }) => { 15 | if (isChildrenEmpty(children)) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
27 | {children} 28 |
29 | ); 30 | }; 31 | 32 | ModalBody.defaultProps = { 33 | children: null, 34 | scrolling: 'auto', 35 | }; 36 | 37 | ModalBody.propTypes = { 38 | /** 39 | * Nested elements. Supported types are: 40 | * 41 | * * `ModalContent` 42 | * * `ScrollView` (`scrolling: 'custom'` must be set) 43 | * 44 | * You can also provide a custom component responsible for scrolling and displaying content correctly. 45 | * At most one nested element is allowed. If none are provided nothing is rendered. 46 | */ 47 | children: PropTypes.node, 48 | /** 49 | * Scrolling mode: 50 | * 51 | * - `auto`: scrolling is enabled on ModalBody. 52 | * - `custom`: use if providing a custom scrolling component, e.g. an instance of `ScrollView`. 53 | * - `none`: scrolling is disabled on ModalBody and the entire Modal is scrollable instead. 54 | */ 55 | scrolling: PropTypes.oneOf(['auto', 'custom', 'none']), 56 | }; 57 | 58 | export const ModalBodyWithGlobalProps = withGlobalProps(ModalBody, 'ModalBody'); 59 | 60 | export default ModalBodyWithGlobalProps; 61 | -------------------------------------------------------------------------------- /src/components/Modal/ModalBody.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Intentionally do not provide a fallback value for the border color. Setting a fallback value (e.g. `transparent`) 2 | // will result in the border being skewed at both ends. 3 | 4 | @use "settings"; 5 | 6 | @layer components.modal { 7 | .root { 8 | flex: 1 1 auto; 9 | border-inline: settings.$border-width solid var(--rui-local-border-color); // 1. 10 | 11 | &:first-child { 12 | border-top: settings.$border-width solid var(--rui-local-border-color); // 1. 13 | border-top-left-radius: settings.$border-radius; 14 | border-top-right-radius: settings.$border-radius; 15 | } 16 | 17 | &:last-child { 18 | border-bottom: settings.$border-width solid var(--rui-local-border-color); // 1. 19 | border-bottom-right-radius: settings.$border-radius; 20 | border-bottom-left-radius: settings.$border-radius; 21 | } 22 | } 23 | 24 | .isRootScrollingAuto, 25 | .isRootScrollingCustom { 26 | min-height: 0; 27 | } 28 | 29 | .isRootScrollingAuto { 30 | overflow-y: auto; 31 | overscroll-behavior: contain; 32 | } 33 | 34 | .isRootScrollingCustom { 35 | display: flex; 36 | flex-direction: column; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Modal/ModalCloseButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { useContext } from 'react'; 3 | import { TranslationsContext } from '../../providers/translations'; 4 | import { withGlobalProps } from '../../providers/globalProps'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import styles from './ModalCloseButton.module.scss'; 7 | 8 | export const ModalCloseButton = React.forwardRef((props, ref) => { 9 | const { 10 | disabled, 11 | ...restProps 12 | } = props; 13 | 14 | const translations = useContext(TranslationsContext); 15 | 16 | return ( 17 | 27 | ); 28 | }); 29 | 30 | ModalCloseButton.defaultProps = { 31 | disabled: false, 32 | }; 33 | 34 | ModalCloseButton.propTypes = { 35 | /** 36 | * If `true`, close button will be disabled. 37 | */ 38 | disabled: PropTypes.bool, 39 | }; 40 | 41 | export const ModalCloseButtonWithGlobalProps = withGlobalProps(ModalCloseButton, 'ModalCloseButton'); 42 | 43 | export default ModalCloseButtonWithGlobalProps; 44 | -------------------------------------------------------------------------------- /src/components/Modal/ModalCloseButton.module.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../styles/theme/typography"; 3 | @use "../../styles/tools/accessibility"; 4 | @use "../../styles/tools/reset"; 5 | @use "../../styles/tools/spacing"; 6 | 7 | @layer components.modal { 8 | .root { 9 | @include reset.button(); 10 | @include accessibility.min-tap-target(); 11 | 12 | font-size: map.get(typography.$font-size-values, 4); 13 | line-height: 1; 14 | color: inherit; 15 | 16 | &:disabled { 17 | cursor: var(--rui-cursor-not-allowed); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Modal/ModalContent.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; 6 | import styles from './ModalContent.module.scss'; 7 | 8 | export const ModalContent = ({ 9 | children, 10 | ...restProps 11 | }) => { 12 | if (isChildrenEmpty(children)) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
21 | {children} 22 |
23 | ); 24 | }; 25 | 26 | ModalContent.defaultProps = { 27 | children: null, 28 | }; 29 | 30 | ModalContent.propTypes = { 31 | /** 32 | * Content of the modal. 33 | */ 34 | children: PropTypes.node, 35 | }; 36 | 37 | export const ModalContentWithGlobalProps = withGlobalProps(ModalContent, 'ModalContent'); 38 | 39 | export default ModalContentWithGlobalProps; 40 | -------------------------------------------------------------------------------- /src/components/Modal/ModalContent.module.scss: -------------------------------------------------------------------------------- 1 | @use "theme"; 2 | 3 | @layer components.modal { 4 | .root { 5 | padding: theme.$padding-y theme.$padding-x; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Modal/ModalFooter.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { getJustifyClassName } from './_helpers/getJustifyClassName'; 7 | import styles from './ModalFooter.module.scss'; 8 | 9 | export const ModalFooter = ({ 10 | children, 11 | justify, 12 | ...restProps 13 | }) => ( 14 |
21 | {children} 22 |
23 | ); 24 | 25 | ModalFooter.defaultProps = { 26 | justify: 'center', 27 | }; 28 | 29 | ModalFooter.propTypes = { 30 | /** 31 | * Content of the footer (preferably nested `Button` elements). 32 | */ 33 | children: PropTypes.node.isRequired, 34 | /** 35 | * Horizontal alignment (distribution) of individual buttons. 36 | */ 37 | justify: PropTypes.oneOf(['start', 'center', 'end', 'space-between', 'stretch']), 38 | }; 39 | 40 | export const ModalFooterWithGlobalProps = withGlobalProps(ModalFooter, 'ModalFooter'); 41 | 42 | export default ModalFooterWithGlobalProps; 43 | -------------------------------------------------------------------------------- /src/components/Modal/ModalFooter.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Intentionally do not provide a fallback value for the border color. Setting a fallback value (e.g. `transparent`) 2 | // will result in the border being skewed at both ends. 3 | 4 | @use "settings"; 5 | @use "theme"; 6 | 7 | @layer components.modal { 8 | .root { 9 | display: flex; 10 | flex: none; 11 | flex-wrap: wrap; 12 | gap: theme.$footer-gap; 13 | align-items: center; 14 | padding: theme.$padding-y theme.$padding-x; 15 | border: settings.$border-width solid var(--rui-local-border-color); // 1. 16 | border-top: theme.$separator-width solid var(--rui-local-border-color, #{theme.$separator-color}); 17 | border-bottom-right-radius: settings.$border-radius; 18 | border-bottom-left-radius: settings.$border-radius; 19 | background: var(--rui-local-background-color, #{theme.$footer-background}); 20 | } 21 | 22 | .isRootJustifiedToStart { 23 | justify-content: flex-start; 24 | } 25 | 26 | .isRootJustifiedToCenter { 27 | justify-content: center; 28 | } 29 | 30 | .isRootJustifiedToEnd { 31 | justify-content: flex-end; 32 | } 33 | 34 | .isRootJustifiedToSpaceBetween { 35 | justify-content: space-between; 36 | } 37 | 38 | .isRootJustifiedToStretch { 39 | display: block; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Modal/ModalHeader.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { getJustifyClassName } from './_helpers/getJustifyClassName'; 7 | import styles from './ModalHeader.module.scss'; 8 | 9 | export const ModalHeader = ({ 10 | children, 11 | justify, 12 | ...restProps 13 | }) => ( 14 |
21 | {children} 22 |
23 | ); 24 | 25 | ModalHeader.defaultProps = { 26 | justify: 'space-between', 27 | }; 28 | 29 | ModalHeader.propTypes = { 30 | /** 31 | * Content of the header (preferably ModalTitle and ModalCloseButton). 32 | */ 33 | children: PropTypes.node.isRequired, 34 | /** 35 | * Horizontal alignment (distribution) of individual buttons. 36 | */ 37 | justify: PropTypes.oneOf(['start', 'center', 'end', 'space-between', 'stretch']), 38 | }; 39 | 40 | export const ModalHeaderWithGlobalProps = withGlobalProps(ModalHeader, 'ModalHeader'); 41 | 42 | export default ModalHeaderWithGlobalProps; 43 | -------------------------------------------------------------------------------- /src/components/Modal/ModalHeader.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Intentionally do not provide a fallback value for the border color. Setting a fallback value (e.g. `transparent`) 2 | // will result in the border being skewed at both ends. 3 | 4 | @use "settings"; 5 | @use "theme"; 6 | 7 | @layer components.modal { 8 | .root { 9 | display: flex; 10 | flex: none; 11 | gap: theme.$header-gap; 12 | align-items: baseline; 13 | padding: theme.$padding-y theme.$padding-x; 14 | border: settings.$border-width solid var(--rui-local-border-color); // 1. 15 | border-bottom: theme.$separator-width solid var(--rui-local-border-color, #{theme.$separator-color}); 16 | border-top-left-radius: settings.$border-radius; 17 | border-top-right-radius: settings.$border-radius; 18 | } 19 | 20 | .isRootJustifiedToStart { 21 | justify-content: flex-start; 22 | } 23 | 24 | .isRootJustifiedToCenter { 25 | justify-content: center; 26 | } 27 | 28 | .isRootJustifiedToEnd { 29 | justify-content: flex-end; 30 | } 31 | 32 | .isRootJustifiedToSpaceBetween { 33 | justify-content: space-between; 34 | } 35 | 36 | .isRootJustifiedToStretch { 37 | display: block; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Modal/ModalTitle.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import styles from './ModalTitle.module.scss'; 6 | 7 | export const ModalTitle = ({ 8 | children, 9 | level, 10 | ...restProps 11 | }) => { 12 | const HeadingTag = `h${level}`; 13 | 14 | return ( 15 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | ModalTitle.defaultProps = { 25 | level: 2, 26 | }; 27 | 28 | ModalTitle.propTypes = { 29 | /** 30 | * Content of the header (preferably ModalTitle and ModalCloseButton). 31 | */ 32 | children: PropTypes.node.isRequired, 33 | /** 34 | * Optional heading level. Preferably `1` or `2` should be used, see 35 | * [W3C recommendation](https://github.com/w3c/aria-practices/issues/551#issuecomment-365134527). 36 | */ 37 | level: PropTypes.number, 38 | }; 39 | 40 | export const ModalTitleWithGlobalProps = withGlobalProps(ModalTitle, 'ModalTitle'); 41 | 42 | export default ModalTitleWithGlobalProps; 43 | -------------------------------------------------------------------------------- /src/components/Modal/ModalTitle.module.scss: -------------------------------------------------------------------------------- 1 | @use "settings"; 2 | 3 | @layer components.modal { 4 | .root { 5 | margin-block: 0; 6 | font-size: settings.$title-font-size; 7 | 8 | &:not(:last-child) { 9 | margin-bottom: 0; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalBody.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, within, 4 | } from '@testing-library/react'; 5 | import { ModalBody } from '../index'; 6 | 7 | const mandatoryProps = { 8 | children:
content text
, 9 | }; 10 | 11 | describe('rendering', () => { 12 | it.each([ 13 | [ 14 | { 15 | children: ( 16 | <> 17 |
content text 1
18 |
content text 2
19 | 20 | ), 21 | }, 22 | (rootElement) => { 23 | expect(within(rootElement).getByText('content text 1')); 24 | expect(within(rootElement).getByText('content text 2')); 25 | }, 26 | ], 27 | [ 28 | { 29 | scrolling: 'auto', 30 | }, 31 | (rootElement) => { 32 | expect(rootElement).toHaveClass('isRootScrollingAuto'); 33 | }, 34 | ], 35 | [ 36 | { 37 | scrolling: 'custom', 38 | }, 39 | (rootElement) => { 40 | expect(rootElement).toHaveClass('isRootScrollingCustom'); 41 | }, 42 | ], 43 | [ 44 | { 45 | scrolling: 'none', 46 | }, 47 | (rootElement) => { 48 | expect(rootElement).not.toHaveClass('isRootScrollingAuto'); 49 | expect(rootElement).not.toHaveClass('isRootScrollingCustom'); 50 | }, 51 | ], 52 | ])('renders with props: "%s"', (testedProps, assert) => { 53 | const dom = render(( 54 | 58 | )); 59 | 60 | assert(dom.container.firstChild); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalCloseButton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | screen, 5 | } from '@testing-library/react'; 6 | import userEvent from '@testing-library/user-event'; 7 | import { ModalCloseButton } from '../ModalCloseButton'; 8 | import { refPropTest } from '../../../../tests/propTests/refPropTest'; 9 | 10 | describe('rendering', () => { 11 | it.each([ 12 | [ 13 | { disabled: true }, 14 | (rootElement) => expect(rootElement).toBeDisabled(), 15 | ], 16 | ...refPropTest(React.createRef()), 17 | ])('renders with props: "%s"', (testedProps, assert) => { 18 | const dom = render(( 19 | 22 | )); 23 | 24 | assert(dom.container.firstChild); 25 | }); 26 | }); 27 | 28 | describe('functionality', () => { 29 | it('calls synthetic event onClick()', async () => { 30 | const spy = jest.fn(); 31 | render(( 32 | 35 | )); 36 | 37 | await userEvent.click(screen.getByRole('button')); 38 | expect(spy).toHaveBeenCalled(); 39 | }); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalContent.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { ModalContent } from '../ModalContent'; 7 | 8 | const mandatoryProps = { 9 | children:
content text
, 10 | }; 11 | 12 | describe('rendering', () => { 13 | it.each([ 14 | [ 15 | { 16 | children: ( 17 | <> 18 |
content text 1
19 |
content text 2
20 | 21 | ), 22 | }, 23 | (rootElement) => { 24 | expect(within(rootElement).getByText('content text 1')); 25 | expect(within(rootElement).getByText('content text 2')); 26 | }, 27 | ], 28 | ])('renders with props: "%s"', (testedProps, assert) => { 29 | const dom = render(( 30 | 34 | )); 35 | 36 | assert(dom.container.firstChild); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalFooter.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { ModalFooter } from '../ModalFooter'; 7 | import { justifyPropTest } from '../../../../tests/propTests/justifyPropTest'; 8 | 9 | const mandatoryProps = { 10 | children:
content text
, 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | [ 16 | { 17 | children: ( 18 | <> 19 |
content text 1
20 |
content text 2
21 | 22 | ), 23 | }, 24 | (rootElement) => { 25 | expect(within(rootElement).getByText('content text 1')); 26 | expect(within(rootElement).getByText('content text 2')); 27 | }, 28 | ], 29 | ...justifyPropTest('Root'), 30 | ])('renders with props: "%s"', (testedProps, assert) => { 31 | const dom = render(( 32 | 36 | )); 37 | 38 | assert(dom.container.firstChild); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalHeader.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { ModalHeader } from '../ModalHeader'; 7 | import { justifyPropTest } from '../../../../tests/propTests/justifyPropTest'; 8 | 9 | const mandatoryProps = { 10 | children:
content text
, 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | [ 16 | {}, 17 | (rootElement) => { 18 | expect(within(rootElement).getByText('content text')); 19 | }, 20 | ], 21 | ...justifyPropTest('Root'), 22 | ])('renders with props: "%s"', (testedProps, assert) => { 23 | const dom = render(( 24 | 28 | )); 29 | 30 | assert(dom.container.firstChild); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/Modal/__tests__/ModalTitle.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { ModalTitle } from '../ModalTitle'; 7 | 8 | const mandatoryProps = { 9 | children:
content text
, 10 | }; 11 | 12 | describe('rendering', () => { 13 | it.each([ 14 | [ 15 | { 16 | children: ( 17 | <> 18 |
content text 1
19 |
content text 2
20 | 21 | ), 22 | }, 23 | (rootElement) => { 24 | expect(within(rootElement).getByText('content text 1')); 25 | expect(within(rootElement).getByText('content text 2')); 26 | }, 27 | ], 28 | [ 29 | { 30 | level: 1, 31 | }, 32 | (rootElement) => { 33 | expect(rootElement.tagName).toEqual('H1'); 34 | }, 35 | ], 36 | ])('renders with props: "%s"', (testedProps, assert) => { 37 | const dom = render(( 38 | 42 | )); 43 | 44 | assert(dom.container.firstChild); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/Modal/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 6 | 100% { 7 | opacity: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/dialogOnCancelHandler.js: -------------------------------------------------------------------------------- 1 | // Disable coverage for the following function 2 | /* istanbul ignore next */ 3 | 4 | /** 5 | * Handles the cancel event of the dialog which is fired when the user presses the Escape key or triggers cancel event 6 | * by native dialog mechanism. 7 | * 8 | * It prevents the default behaviour of the native dialog and closes the dialog manually by clicking the close button, 9 | * if the close button is not disabled. 10 | * 11 | * @param e 12 | * @param closeButtonRef 13 | * @param onCancelHandler 14 | */ 15 | export const dialogOnCancelHandler = (e, closeButtonRef, onCancelHandler = undefined) => { 16 | // Prevent the default behaviour of the event as we want to close dialog manually. 17 | e.preventDefault(); 18 | 19 | // If the close button is not disabled, close the modal. 20 | if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) { 21 | closeButtonRef.current.click(); 22 | } 23 | 24 | // This is a custom handler that is passed as a prop to the Modal component 25 | if (onCancelHandler) { 26 | onCancelHandler(e); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/dialogOnClickHandler.js: -------------------------------------------------------------------------------- 1 | // Disable coverage for the following function 2 | /* istanbul ignore next */ 3 | 4 | /** 5 | * Handles the click event of the dialog which is fired when the user clicks on the dialog or on its descendants. 6 | * 7 | * This handler is used to close the dialog when the user clicks on the backdrop, if it is allowed to close 8 | * on backdrop click and the close button is not disabled. 9 | * 10 | * @param e 11 | * @param closeButtonRef 12 | * @param dialogRef 13 | * @param allowCloseOnBackdropClick 14 | */ 15 | export const dialogOnClickHandler = ( 16 | e, 17 | closeButtonRef, 18 | dialogRef, 19 | allowCloseOnBackdropClick, 20 | ) => { 21 | // If it is not allowed to close modal on backdrop click, do nothing. 22 | if (!allowCloseOnBackdropClick) { 23 | return; 24 | } 25 | 26 | // Detection of the click on the backdrop is based on the following conditions: 27 | // 1. The click target is the dialog itself. This prevents detection of clicks on the dialog's children. 28 | // 2. The click is outside the dialog's boundaries. 29 | const dialogRect = dialogRef.current.getBoundingClientRect(); 30 | const isClickedOnBackdrop = dialogRef.current === e.target && ( 31 | e.clientX < dialogRect.left 32 | || e.clientX > dialogRect.right 33 | || e.clientY < dialogRect.top 34 | || e.clientY > dialogRect.bottom 35 | ); 36 | 37 | // If user does not click on the backdrop, do nothing. 38 | if (!isClickedOnBackdrop) { 39 | return; 40 | } 41 | 42 | // If the close button is not disabled, close the modal. 43 | if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) { 44 | closeButtonRef.current.click(); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/dialogOnCloseHandler.js: -------------------------------------------------------------------------------- 1 | // Disable coverage for the following function 2 | /* istanbul ignore next */ 3 | 4 | /** 5 | * Handles the close event of the dialog which is fired when the user presses the Escape key or triggers close event 6 | * by native dialog mechanism. 7 | * 8 | * It prevents the default behaviour of the native dialog and closes the dialog manually by clicking the close button, 9 | * if the close button is not disabled. 10 | * 11 | * @param e 12 | * @param closeButtonRef 13 | * @param onCloseHandler 14 | */ 15 | export const dialogOnCloseHandler = (e, closeButtonRef, onCloseHandler = undefined) => { 16 | // Prevent the default behaviour of the event as we want to close dialog manually. 17 | e.preventDefault(); 18 | 19 | // If the close button is not disabled, close the modal. 20 | if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) { 21 | closeButtonRef.current.click(); 22 | } 23 | 24 | // This is a custom handler that is passed as a prop to the Modal component 25 | if (onCloseHandler) { 26 | onCloseHandler(e); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/getJustifyClassName.js: -------------------------------------------------------------------------------- 1 | export const getJustifyClassName = (value, styles) => { 2 | if (value === 'start') { 3 | return styles.isRootJustifiedToStart; 4 | } 5 | 6 | if (value === 'center') { 7 | return styles.isRootJustifiedToCenter; 8 | } 9 | 10 | if (value === 'end') { 11 | return styles.isRootJustifiedToEnd; 12 | } 13 | 14 | if (value === 'space-between') { 15 | return styles.isRootJustifiedToSpaceBetween; 16 | } 17 | 18 | return styles.isRootJustifiedToStretch; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/getPositionClassName.js: -------------------------------------------------------------------------------- 1 | export const getPositionClassName = (modalPosition, styles) => { 2 | if (modalPosition === 'top') { 3 | return styles.isRootPositionTop; 4 | } 5 | 6 | return null; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/getScrollingClassName.js: -------------------------------------------------------------------------------- 1 | export const getScrollingClassName = (type, styles) => { 2 | if (type === 'auto') { 3 | return styles.isRootScrollingAuto; 4 | } 5 | 6 | if (type === 'custom') { 7 | return styles.isRootScrollingCustom; 8 | } 9 | 10 | return null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Modal/_helpers/getSizeClassName.js: -------------------------------------------------------------------------------- 1 | export const getSizeClassName = (modalSize, styles) => { 2 | if (modalSize === 'small') { 3 | return styles.isRootSizeSmall; 4 | } 5 | 6 | if (modalSize === 'medium') { 7 | return styles.isRootSizeMedium; 8 | } 9 | 10 | if (modalSize === 'large') { 11 | return styles.isRootSizeLarge; 12 | } 13 | 14 | if (modalSize === 'fullscreen') { 15 | return styles.isRootSizeFullscreen; 16 | } 17 | 18 | return styles.isRootSizeAuto; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Modal/_hooks/useModalFocus.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useModalFocus = ( 4 | autoFocus, 5 | dialogRef, 6 | primaryButtonRef, 7 | ) => { 8 | useEffect( 9 | () => { 10 | // Following code finds all focusable elements and among them first not disabled form 11 | // field element (input, textarea or select) or primary button and focuses it. This is 12 | // necessary to have focus on one of those elements to be able to submit the form 13 | // by pressing Enter key. If there are neither, it tries to focus any other focusable 14 | // elements. In case there are none or `autoFocus` is disabled, dialogElement 15 | // (Modal itself) is focused. 16 | 17 | const dialogElement = dialogRef.current; 18 | 19 | if (dialogElement == null) { 20 | return () => {}; 21 | } 22 | 23 | const childrenFocusableElements = Array.from( 24 | dialogElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'), 25 | ); 26 | 27 | const firstFocusableElement = childrenFocusableElements[0]; 28 | 29 | if (!autoFocus || childrenFocusableElements.length === 0) { 30 | dialogElement.tabIndex = -1; 31 | dialogElement.focus(); 32 | return () => {}; 33 | } 34 | 35 | const firstFormFieldEl = childrenFocusableElements.find( 36 | (element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled, 37 | ); 38 | 39 | if (firstFormFieldEl) { 40 | firstFormFieldEl.focus(); 41 | return () => {}; 42 | } 43 | 44 | if (primaryButtonRef?.current != null && primaryButtonRef?.current?.disabled === false) { 45 | primaryButtonRef.current.focus(); 46 | return () => {}; 47 | } 48 | 49 | firstFocusableElement.focus(); 50 | 51 | return () => {}; 52 | }, 53 | [ 54 | autoFocus, 55 | dialogRef, 56 | primaryButtonRef, 57 | ], 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/Modal/_hooks/useModalScrollPrevention.js: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | 3 | export const useModalScrollPrevention = (preventScrollUnderneath) => { 4 | useLayoutEffect( 5 | () => { 6 | if (preventScrollUnderneath === 'off') { 7 | return () => {}; 8 | } 9 | 10 | if (preventScrollUnderneath instanceof HTMLElement) { 11 | const scrollableElement = preventScrollUnderneath; 12 | 13 | const scrollbarWidth = Math.abs(window.innerWidth - window.document.documentElement.clientWidth); 14 | const prevOverflow = scrollableElement.style.overflow; 15 | const prevPaddingRight = scrollableElement.style.paddingRight; 16 | 17 | scrollableElement.style.overflow = 'hidden'; 18 | 19 | if (Number.isNaN(parseInt(prevPaddingRight, 10))) { 20 | scrollableElement.style.paddingRight = `${scrollbarWidth}px`; 21 | } else { 22 | scrollableElement.style.paddingRight = `calc(${prevPaddingRight} + ${scrollbarWidth}px)`; 23 | } 24 | 25 | return () => { 26 | scrollableElement.style.overflow = prevOverflow; 27 | scrollableElement.style.paddingRight = prevPaddingRight; 28 | }; 29 | } 30 | 31 | preventScrollUnderneath?.start(); 32 | 33 | return preventScrollUnderneath?.reset; 34 | }, 35 | [preventScrollUnderneath], 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Modal/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../styles/settings/collections"; 3 | @use "../../styles/theme/borders"; 4 | @use "../../styles/theme/typography"; 5 | 6 | $border-width: borders.$width; 7 | $border-radius: borders.$radius-2; 8 | $title-font-size: map.get(typography.$font-size-values, 2); 9 | $colors: collections.$feedback-colors; 10 | $themeable-properties: border-color, background-color; 11 | -------------------------------------------------------------------------------- /src/components/Modal/_theme.scss: -------------------------------------------------------------------------------- 1 | $padding-x: var(--rui-Modal__padding-x); 2 | $padding-y: var(--rui-Modal__padding-y); 3 | $background: var(--rui-Modal__background); 4 | $box-shadow: var(--rui-Modal__box-shadow); 5 | $separator-width: var(--rui-Modal__separator__width); 6 | $separator-color: var(--rui-Modal__separator__color); 7 | $header-gap: var(--rui-Modal__header__gap); 8 | $footer-background: var(--rui-Modal__footer__background); 9 | $footer-gap: var(--rui-Modal__footer__gap); 10 | $backdrop-background: var(--rui-Modal__backdrop__background); 11 | $outer-spacing-xs: var(--rui-Modal__outer-spacing--xs); 12 | $outer-spacing-sm: var(--rui-Modal__outer-spacing--sm); 13 | $animation-duration: var(--rui-Modal__animation__duration); 14 | 15 | $sizes: ( 16 | auto: ( 17 | min-width: var(--rui-Modal--auto__min-width), 18 | max-width: var(--rui-Modal--auto__max-width), 19 | ), 20 | small: ( 21 | width: var(--rui-Modal--small__width), 22 | ), 23 | medium: ( 24 | width: var(--rui-Modal--medium__width), 25 | ), 26 | large: ( 27 | width: var(--rui-Modal--large__width), 28 | ), 29 | fullscreen: ( 30 | width: var(--rui-Modal--fullscreen__width), 31 | height: var(--rui-Modal--fullscreen__height), 32 | ), 33 | ); 34 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | export { default as Modal } from './Modal'; 2 | export { default as ModalBody } from './ModalBody'; 3 | export { default as ModalCloseButton } from './ModalCloseButton'; 4 | export { default as ModalContent } from './ModalContent'; 5 | export { default as ModalHeader } from './ModalHeader'; 6 | export { default as ModalFooter } from './ModalFooter'; 7 | export { default as ModalTitle } from './ModalTitle'; 8 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import styles from './Paper.module.scss'; 7 | 8 | export const Paper = ({ 9 | children, 10 | muted, 11 | raised, 12 | ...restProps 13 | }) => ( 14 |
22 | {children} 23 |
24 | ); 25 | 26 | Paper.defaultProps = { 27 | muted: false, 28 | raised: false, 29 | }; 30 | 31 | Paper.propTypes = { 32 | /** 33 | * Content to be placed onto Paper. 34 | */ 35 | children: PropTypes.node.isRequired, 36 | /** 37 | * Visually suppress Paper. 38 | */ 39 | muted: PropTypes.bool, 40 | /** 41 | * Add shadow to pull the Paper above surface. 42 | */ 43 | raised: PropTypes.bool, 44 | }; 45 | 46 | export const PaperWithGlobalProps = withGlobalProps(Paper, 'Paper'); 47 | 48 | export default PaperWithGlobalProps; 49 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.module.scss: -------------------------------------------------------------------------------- 1 | @use "theme"; 2 | 3 | @layer components.paper { 4 | .root { 5 | padding: theme.$padding; 6 | border: theme.$border-width solid theme.$border-color; 7 | border-radius: theme.$border-radius; 8 | background-color: theme.$background-color; 9 | } 10 | 11 | .isRootMuted { 12 | background-color: theme.$muted-background-color; 13 | opacity: theme.$muted-opacity; 14 | } 15 | 16 | .isRootRaised { 17 | box-shadow: theme.$raised-box-shadow; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Paper/__tests__/Paper.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { raisedPropTest } from '../../../../tests/propTests/raisedPropTest'; 7 | import { Paper } from '../Paper'; 8 | 9 | const defaultProps = { 10 | children: 'sample text', 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | [ 16 | { children:
content text
}, 17 | (rootElement) => expect(within(rootElement).getByText('content text')), 18 | ], 19 | [ 20 | { muted: true }, 21 | (rootElement) => expect(rootElement).toHaveClass('isRootMuted'), 22 | ], 23 | [ 24 | { muted: false }, 25 | (rootElement) => expect(rootElement).not.toHaveClass('isRootMuted'), 26 | ], 27 | ...raisedPropTest, 28 | ])('renders with props: "%s"', (testedProps, assert) => { 29 | const dom = render(( 30 | 34 | )); 35 | 36 | assert(dom.container.firstChild); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Paper/_theme.scss: -------------------------------------------------------------------------------- 1 | $padding: var(--rui-Paper__padding); 2 | $border-width: var(--rui-Paper__border-width); 3 | $border-color: var(--rui-Paper__border-color); 4 | $border-radius: var(--rui-Paper__border-radius); 5 | $background-color: var(--rui-Paper__background-color); 6 | $muted-background-color: var(--rui-Paper--muted__background-color); 7 | $muted-opacity: var(--rui-Paper--muted__opacity); 8 | $raised-box-shadow: var(--rui-Paper--raised__box-shadow); 9 | -------------------------------------------------------------------------------- /src/components/Paper/index.js: -------------------------------------------------------------------------------- 1 | export { default as Paper } from './Paper'; 2 | -------------------------------------------------------------------------------- /src/components/Popover/PopoverWrapper.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import styles from './PopoverWrapper.module.scss'; 6 | 7 | export const PopoverWrapper = ({ 8 | children, 9 | tag: Tag, 10 | ...restProps 11 | }) => ( 12 | 16 | {children} 17 | 18 | ); 19 | 20 | PopoverWrapper.defaultProps = { 21 | tag: 'div', 22 | }; 23 | 24 | PopoverWrapper.propTypes = { 25 | /** 26 | * Popover reference and the Popover itself. 27 | */ 28 | children: PropTypes.node.isRequired, 29 | /** 30 | * HTML tag to render. Can be any valid HTML tag of your choice, usually a 31 | * [block-level element](https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements). 32 | */ 33 | tag: PropTypes.string, 34 | }; 35 | 36 | export const PopoverWrapperWithContext = withGlobalProps(PopoverWrapper, 'PopoverWrapper'); 37 | 38 | export default PopoverWrapperWithContext; 39 | -------------------------------------------------------------------------------- /src/components/Popover/PopoverWrapper.module.scss: -------------------------------------------------------------------------------- 1 | @layer components.popover { 2 | .root { 3 | position: relative; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Popover/__tests__/PopoverWrapper.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { tagPropTest } from '../../../../tests/propTests/tagPropTest'; 7 | import { PopoverWrapper } from '../PopoverWrapper'; 8 | 9 | const mandatoryProps = { 10 | children:
content text
, 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | [ 16 | { children:
content text
}, 17 | (rootElement) => expect(within(rootElement).getByText('content text')), 18 | ], 19 | ...tagPropTest, 20 | ])('renders with props: "%s"', (testedProps, assert) => { 21 | const dom = render(( 22 | 26 | )); 27 | 28 | assert(dom.container.firstChild); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Popover/_helpers/cleanPlacementStyle.js: -------------------------------------------------------------------------------- 1 | export default (placementStyle) => { 2 | const validProps = [ 3 | 'position', 4 | 'inset', 5 | 'inset-inline-start', 6 | 'inset-inline-end', 7 | 'inset-block-start', 8 | 'inset-block-end', 9 | 'top', 10 | 'right', 11 | 'bottom', 12 | 'left', 13 | 'translate', 14 | 'transform-origin', 15 | ]; 16 | 17 | return Object.fromEntries( 18 | Object.entries(placementStyle).filter(([prop]) => validProps.includes(prop)), 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Popover/_helpers/getRootAlignmentClassName.js: -------------------------------------------------------------------------------- 1 | export default (placement, styles) => { 2 | const alignment = placement.split('-')[1]; 3 | 4 | if (alignment === 'start') { 5 | return styles.isRootAtStart; 6 | } 7 | 8 | if (alignment === 'end') { 9 | return styles.isRootAtEnd; 10 | } 11 | 12 | return styles.isRootAtCenter; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Popover/_helpers/getRootSideClassName.js: -------------------------------------------------------------------------------- 1 | export default (placement, styles) => { 2 | const side = placement.split('-')[0]; 3 | 4 | if (side === 'top') { 5 | return styles.isRootAtTop; 6 | } 7 | 8 | if (side === 'right') { 9 | return styles.isRootAtRight; 10 | } 11 | 12 | if (side === 'bottom') { 13 | return styles.isRootAtBottom; 14 | } 15 | 16 | return styles.isRootAtLeft; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Popover/_theme.scss: -------------------------------------------------------------------------------- 1 | // 1. Height must be linked to width to create 90-degree angle. 2 | 3 | @use "sass:math"; 4 | 5 | $max-width: var(--rui-Popover__max-width); 6 | $padding: var(--rui-Popover__padding); 7 | $border-width: var(--rui-Popover__border-width); 8 | $border-color: var(--rui-Popover__border-color); 9 | $border-radius: var(--rui-Popover__border-radius); 10 | $color: var(--rui-Popover__color); 11 | $background-color: var(--rui-Popover__background-color); 12 | $box-shadow: var(--rui-Popover__box-shadow); 13 | 14 | $arrow-safe-rendering-overlap: 1px; 15 | $arrow-gap: 1px; 16 | $arrow-width: calc(1rem + #{$arrow-safe-rendering-overlap * 2}); 17 | $arrow-height: calc($arrow-width / 2); // 1. 18 | $arrow-corner-offset: 0.75rem; 19 | -------------------------------------------------------------------------------- /src/components/Popover/index.js: -------------------------------------------------------------------------------- 1 | export { default as Popover } from './Popover'; 2 | export { default as PopoverWrapper } from './PopoverWrapper'; 3 | -------------------------------------------------------------------------------- /src/components/Radio/index.js: -------------------------------------------------------------------------------- 1 | export { default as Radio } from './Radio'; 2 | -------------------------------------------------------------------------------- /src/components/ScrollView/_helpers/getElementsPositionDifference.js: -------------------------------------------------------------------------------- 1 | export const getElementsPositionDifference = (contentEl, viewportEl) => { 2 | const contentPosition = contentEl.current.getBoundingClientRect(); 3 | const viewportPosition = viewportEl.current.getBoundingClientRect(); 4 | 5 | return { 6 | bottom: contentPosition.bottom - viewportPosition.bottom, 7 | left: contentPosition.left - viewportPosition.left, 8 | right: contentPosition.right - viewportPosition.right, 9 | top: contentPosition.top - viewportPosition.top, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ScrollView/_hooks/useLoadResizeHook.js: -------------------------------------------------------------------------------- 1 | import { 2 | useLayoutEffect, 3 | useRef, 4 | } from 'react'; 5 | import { getElementsPositionDifference } from '../_helpers/getElementsPositionDifference'; 6 | 7 | export const useLoadResize = (effect, dependencies, contentEl, viewportEl, wait) => { 8 | const throttleTimeout = useRef(null); 9 | 10 | const callBack = (wasDelayed = false) => { 11 | effect(getElementsPositionDifference(contentEl, viewportEl)); 12 | 13 | if (wasDelayed) { 14 | throttleTimeout.current = null; 15 | } 16 | }; 17 | 18 | useLayoutEffect(() => { 19 | const handleLoadResize = () => { 20 | if (wait) { 21 | if (throttleTimeout.current === null) { 22 | throttleTimeout.current = setTimeout(callBack, wait, true); 23 | } 24 | } else { 25 | callBack(); 26 | } 27 | }; 28 | 29 | window.addEventListener('load', handleLoadResize); 30 | window.addEventListener('resize', handleLoadResize); 31 | 32 | return () => { 33 | clearTimeout(throttleTimeout.current); 34 | window.removeEventListener('load', handleLoadResize); 35 | window.removeEventListener('resize', handleLoadResize); 36 | }; 37 | }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps 38 | }; 39 | 40 | export default useLoadResize; 41 | -------------------------------------------------------------------------------- /src/components/ScrollView/_hooks/useScrollPositionHook.js: -------------------------------------------------------------------------------- 1 | import { 2 | useLayoutEffect, 3 | useRef, 4 | } from 'react'; 5 | import { getElementsPositionDifference } from '../_helpers/getElementsPositionDifference'; 6 | 7 | export const useScrollPosition = (effect, dependencies, contentEl, viewportEl, wait) => { 8 | const throttleTimeout = useRef(null); 9 | 10 | const callBack = (wasDelayed = false) => { 11 | effect(getElementsPositionDifference(contentEl, viewportEl)); 12 | 13 | if (wasDelayed) { 14 | throttleTimeout.current = null; 15 | } 16 | }; 17 | 18 | useLayoutEffect(() => { 19 | const viewport = viewportEl.current; 20 | 21 | const handleScroll = () => { 22 | if (wait) { 23 | if (throttleTimeout.current === null) { 24 | throttleTimeout.current = setTimeout(callBack, wait, true); 25 | } 26 | } else { 27 | callBack(); 28 | } 29 | }; 30 | 31 | viewport.addEventListener('scroll', handleScroll); 32 | 33 | return () => { 34 | clearTimeout(throttleTimeout.current); 35 | viewport.removeEventListener('scroll', handleScroll); 36 | }; 37 | }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps 38 | }; 39 | 40 | export default useScrollPosition; 41 | -------------------------------------------------------------------------------- /src/components/ScrollView/index.js: -------------------------------------------------------------------------------- 1 | export { default as ScrollView } from './ScrollView'; 2 | -------------------------------------------------------------------------------- /src/components/SelectField/_components/Option/Option.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | export const Option = ({ 5 | disabled, 6 | id, 7 | label, 8 | value, 9 | }) => ( 10 | 17 | ); 18 | 19 | Option.defaultProps = { 20 | disabled: false, 21 | id: undefined, 22 | }; 23 | 24 | Option.propTypes = { 25 | /** 26 | * If `true` the option cannot be selected. 27 | */ 28 | disabled: PropTypes.bool, 29 | /** 30 | * ID of an individual option. 31 | */ 32 | id: PropTypes.string, 33 | /** 34 | * Option label. 35 | */ 36 | label: PropTypes.string.isRequired, 37 | /** 38 | * Option value. 39 | */ 40 | value: PropTypes.oneOfType([ 41 | PropTypes.string, 42 | PropTypes.number, 43 | ]).isRequired, 44 | }; 45 | 46 | export default Option; 47 | -------------------------------------------------------------------------------- /src/components/SelectField/_components/Option/index.js: -------------------------------------------------------------------------------- 1 | export { default as Option } from './Option'; 2 | -------------------------------------------------------------------------------- /src/components/SelectField/index.js: -------------------------------------------------------------------------------- 1 | export { default as SelectField } from './SelectField'; 2 | -------------------------------------------------------------------------------- /src/components/Table/Table.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/tools/transition"; 2 | @use "settings"; 3 | 4 | @layer components.table { 5 | .table { 6 | width: 100%; 7 | border-collapse: collapse; 8 | } 9 | 10 | .tableRow, 11 | .tableHeadRow { 12 | @include transition.add((background-color)); 13 | } 14 | 15 | .tableRow { 16 | background-color: settings.$background-color; 17 | 18 | &:hover { 19 | background-color: settings.$hover-background-color; 20 | } 21 | } 22 | 23 | .tableHeadRow { 24 | background-color: settings.$head-background-color; 25 | 26 | &:hover { 27 | background-color: settings.$head-background-color; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Table/_components/TableBodyCell/TableBodyCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import styles from '../TableCell.module.scss'; 4 | 5 | export const TableBodyCell = ({ 6 | format, 7 | id, 8 | isSortingActive, 9 | value, 10 | }) => ( 11 | 15 | {format ? format(value) : value} 16 | 17 | ); 18 | 19 | TableBodyCell.defaultProps = { 20 | format: undefined, 21 | id: undefined, 22 | isSortingActive: false, 23 | value: null, 24 | }; 25 | 26 | TableBodyCell.propTypes = { 27 | /** 28 | * Function that can be used to process the column data before displaying them. 29 | */ 30 | format: PropTypes.func, 31 | /** 32 | * ID of the HTML element: 33 | */ 34 | id: PropTypes.string, 35 | /** 36 | * If `true`, cell is gray marked as sorted. 37 | */ 38 | isSortingActive: PropTypes.bool, 39 | /** 40 | * Cell value. 41 | */ 42 | // eslint-disable-next-line react/forbid-prop-types 43 | value: PropTypes.any, 44 | }; 45 | 46 | export default TableBodyCell; 47 | -------------------------------------------------------------------------------- /src/components/Table/_components/TableBodyCell/index.js: -------------------------------------------------------------------------------- 1 | export { default as TableBodyCell } from './TableBodyCell'; 2 | -------------------------------------------------------------------------------- /src/components/Table/_components/TableCell.module.scss: -------------------------------------------------------------------------------- 1 | @use "../settings"; 2 | 3 | @layer components.table { 4 | .tableCell, 5 | .tableHeadCell, 6 | .isTableCellSortingActive, 7 | .isTableHeadCellSortingActive { 8 | padding: settings.$cell-padding-y settings.$cell-padding-x; 9 | text-align: left; 10 | border-bottom: settings.$border-width solid settings.$border-color; 11 | } 12 | 13 | .tableHeadCell { 14 | font-weight: settings.$head-font-weight; 15 | border-bottom-width: 2px; 16 | } 17 | 18 | .tableHeadCellLayout { 19 | display: flex; 20 | gap: settings.$cell-padding-x; 21 | align-items: center; 22 | } 23 | 24 | .isTableCellSortingActive, 25 | .isTableHeadCellSortingActive { 26 | background-color: settings.$sorted-background-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Table/_components/TableHeaderCell/index.js: -------------------------------------------------------------------------------- 1 | export { default as TableHeaderCell } from './TableHeaderCell'; 2 | -------------------------------------------------------------------------------- /src/components/Table/_settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../styles/theme/borders"; 3 | @use "../../styles/theme/typography"; 4 | @use "../../styles/tools/spacing"; 5 | 6 | $cell-padding-x: spacing.of(3); 7 | $cell-padding-y: spacing.of(1); 8 | $border-width: borders.$width; 9 | $border-color: var(--rui-color-border-secondary); 10 | $background-color: var(--rui-color-background-basic); 11 | $head-background-color: var(--rui-color-background-basic); 12 | $head-font-weight: map.get(typography.$font-weight-values, bold); 13 | $hover-background-color: var(--rui-color-background-interactive-hover); 14 | $sorted-background-color: var(--rui-color-background-selected); 15 | -------------------------------------------------------------------------------- /src/components/Table/index.js: -------------------------------------------------------------------------------- 1 | export { default as Table } from './Table'; 2 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import styles from './Tabs.module.scss'; 6 | 7 | export const Tabs = ({ 8 | children, 9 | id, 10 | ...restProps 11 | }) => ( 12 | 23 | ); 24 | 25 | Tabs.defaultProps = { 26 | id: undefined, 27 | }; 28 | 29 | Tabs.propTypes = { 30 | /** 31 | * Nested `TabsItem` elements. 32 | */ 33 | children: PropTypes.node.isRequired, 34 | /** 35 | * ID of the root HTML element. It also serves as base for nested element: 36 | * * `__list` 37 | */ 38 | id: PropTypes.string, 39 | }; 40 | 41 | export const TabsWithGlobalProps = withGlobalProps(Tabs, 'Tabs'); 42 | 43 | export default TabsWithGlobalProps; 44 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Use the `clip` value to prevent the content from unwanted vertical scrolling during keyboard navigation. 2 | // 2. Decorative bottom border. 3 | 4 | @use "../../styles/tools/reset"; 5 | @use "theme"; 6 | 7 | @layer components.tabs { 8 | .list { 9 | @include reset.list(); 10 | 11 | position: relative; 12 | display: inline-flex; 13 | min-width: 100%; 14 | padding-right: theme.$padding-x; 15 | padding-left: theme.$padding-x; 16 | overflow-y: clip; // 1. 17 | white-space: nowrap; 18 | 19 | // 2. 20 | &::after { 21 | content: ""; 22 | position: absolute; 23 | right: 0; 24 | bottom: 0; 25 | left: 0; 26 | z-index: 1; 27 | height: theme.$border-bottom-width; 28 | background-color: theme.$border-bottom-color; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Tabs/__tests__/Tabs.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { Tabs } from '../Tabs'; 7 | import { TabsItem } from '../TabsItem'; 8 | 9 | const tab = ( 10 | 14 | ); 15 | 16 | const defaultProps = { 17 | children: 'sample text', 18 | }; 19 | 20 | describe('rendering', () => { 21 | it.each([ 22 | [ 23 | { children: tab }, 24 | (rootElement) => expect(within(rootElement).getByText('label')), 25 | ], 26 | [ 27 | { children: [tab] }, 28 | (rootElement) => expect(within(rootElement).getByText('label')), 29 | ], 30 | [ 31 | { id: 'id' }, 32 | (rootElement) => { 33 | expect(rootElement).toHaveAttribute('id', 'id'); 34 | expect(within(rootElement).getByTestId('id__list')); 35 | }, 36 | ], 37 | ])('renders with props: "%s"', (testedProps, assert) => { 38 | const dom = render(( 39 | 43 | )); 44 | 45 | assert(dom.container.firstChild); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs'; 2 | export { default as TabsItem } from './TabsItem'; 3 | -------------------------------------------------------------------------------- /src/components/Text/Text.module.scss: -------------------------------------------------------------------------------- 1 | // 1. `word-break: break-word` is deprecated in favour of `overflow-wrap: anywhere`, but it's still 2 | // required for Safari. 3 | // https://caniuse.com/mdn-css_properties_overflow-wrap_anywhere 4 | // 5 | // 2. Different approaches are used for single and multiline texts because the latter approach 6 | // doesn't always work for single-line texts. 7 | 8 | @layer components.text { 9 | .isRootClampSingleLine { 10 | display: block; // 2. 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | } 15 | 16 | // stylelint-disable property-no-vendor-prefix, value-no-vendor-prefix 17 | .isRootClampMultiLine { 18 | display: -webkit-box; // 2. 19 | -webkit-line-clamp: var(--rui-custom-lines); 20 | -webkit-box-orient: vertical; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | } 24 | // stylelint-enable property-no-vendor-prefix, value-no-vendor-prefix 25 | 26 | .isRootHyphensAuto { 27 | hyphens: auto; 28 | } 29 | 30 | .isRootHyphensManual { 31 | hyphens: manual; 32 | } 33 | 34 | .isRootWordWrappingAnywhere { 35 | word-break: break-all; 36 | } 37 | 38 | .isRootWordWrappingLongWords { 39 | word-break: break-word; // 1. 40 | overflow-wrap: anywhere; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Text/_helpers/getRootClampClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootClampClassName = (lines, styles) => { 2 | if (lines === 1) { 3 | return styles.isRootClampSingleLine; 4 | } 5 | 6 | if (lines > 1) { 7 | return styles.isRootClampMultiLine; 8 | } 9 | 10 | return null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Text/_helpers/getRootHyphensClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootHyphensClassName = (hyphens, styles) => { 2 | if (hyphens === 'auto') { 3 | return styles.isRootHyphensAuto; 4 | } 5 | 6 | if (hyphens === 'manual') { 7 | return styles.isRootHyphensManual; 8 | } 9 | 10 | return null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Text/_helpers/getRootWordWrappingClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootWordWrappingClassName = (wordWrapping, styles) => { 2 | if (wordWrapping === 'anywhere') { 3 | return styles.isRootWordWrappingAnywhere; 4 | } 5 | 6 | if (wordWrapping === 'long-words') { 7 | return styles.isRootWordWrappingLongWords; 8 | } 9 | 10 | return null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Text/index.js: -------------------------------------------------------------------------------- 1 | export { default as Text } from './Text'; 2 | -------------------------------------------------------------------------------- /src/components/TextArea/index.js: -------------------------------------------------------------------------------- 1 | export { default as TextArea } from './TextArea'; 2 | -------------------------------------------------------------------------------- /src/components/TextField/index.js: -------------------------------------------------------------------------------- 1 | export { default as TextField } from './TextField'; 2 | -------------------------------------------------------------------------------- /src/components/TextLink/TextLink.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import styles from './TextLink.module.scss'; 6 | 7 | export const TextLink = ({ 8 | href, 9 | label, 10 | ...restProps 11 | }) => ( 12 | 17 | {label} 18 | 19 | ); 20 | 21 | TextLink.propTypes = { 22 | /** 23 | * Link's `href` attribute. 24 | */ 25 | href: PropTypes.string.isRequired, 26 | /** 27 | * Link label. 28 | */ 29 | label: PropTypes.string.isRequired, 30 | }; 31 | 32 | export const LinkWithGlobalProps = withGlobalProps(TextLink, 'TextLink'); 33 | 34 | export default LinkWithGlobalProps; 35 | -------------------------------------------------------------------------------- /src/components/TextLink/TextLink.module.scss: -------------------------------------------------------------------------------- 1 | @use "theme"; 2 | 3 | @layer components.text-link { 4 | .root { 5 | text-decoration: theme.$text-decoration; 6 | color: theme.$color; 7 | 8 | &:hover { 9 | text-decoration: theme.$hover-text-decoration; 10 | color: theme.$hover-color; 11 | } 12 | 13 | &:active { 14 | text-decoration: theme.$active-text-decoration; 15 | color: theme.$active-color; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/TextLink/__tests__/Link.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | screen, 5 | } from '@testing-library/react'; 6 | import userEvent from '@testing-library/user-event'; 7 | import { TextLink } from '../TextLink'; 8 | 9 | const mandatoryProps = { 10 | href: '/test/uri', 11 | label: 'link text', 12 | }; 13 | 14 | describe('rendering', () => { 15 | it.each([ 16 | [ 17 | {}, 18 | (rootElement) => expect(rootElement).toHaveAttribute('href', '/test/uri'), 19 | ], 20 | [ 21 | { label: 'other text' }, 22 | (rootElement) => expect(rootElement).toHaveTextContent('other text'), 23 | ], 24 | ])('renders with props: "%s"', (testedProps, assert) => { 25 | const dom = render(( 26 | 30 | )); 31 | 32 | assert(dom.container.firstChild); 33 | }); 34 | }); 35 | 36 | describe('functionality', () => { 37 | it('calls synthetic event onClick()', async () => { 38 | const spy = jest.fn(); 39 | render(( 40 | { 43 | e.preventDefault(); // Prevent the default navigation behavior 44 | spy(); 45 | }} 46 | /> 47 | )); 48 | 49 | await userEvent.click(screen.getByText('link text')); 50 | expect(spy).toHaveBeenCalled(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/TextLink/_theme.scss: -------------------------------------------------------------------------------- 1 | $color: var(--rui-local-link-color, var(--rui-TextLink__color)); 2 | $text-decoration: var(--rui-TextLink__text-decoration); 3 | $hover-color: var(--rui-local-link-color-hover, var(--rui-TextLink--hover__color)); 4 | $hover-text-decoration: var(--rui-TextLink--hover__text-decoration); 5 | $active-color: var(--rui-local-link-color-active, var(--rui-TextLink--active__color)); 6 | $active-text-decoration: var(--rui-TextLink--active__text-decoration); 7 | -------------------------------------------------------------------------------- /src/components/TextLink/index.js: -------------------------------------------------------------------------------- 1 | export { default as TextLink } from './TextLink'; 2 | -------------------------------------------------------------------------------- /src/components/Toggle/Toggle.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/tools/form-fields/foundation"; 2 | @use "../../styles/tools/form-fields/inline-field-elements"; 3 | @use "../../styles/tools/form-fields/inline-field-layout"; 4 | @use "../../styles/tools/form-fields/variants"; 5 | @use "../../styles/tools/accessibility"; 6 | 7 | @layer components.toggle { 8 | // Foundation 9 | .root { 10 | @include foundation.root(); 11 | @include inline-field-layout.root(); 12 | @include inline-field-elements.min-tap-target($type: toggle); 13 | @include variants.visual(check); 14 | } 15 | 16 | .label { 17 | @include foundation.label(); 18 | } 19 | 20 | .field { 21 | @include inline-field-layout.field($type: toggle); 22 | } 23 | 24 | .input { 25 | @include inline-field-elements.check-input($type: toggle); 26 | } 27 | 28 | .helpText, 29 | .validationText { 30 | @include foundation.help-text(); 31 | } 32 | 33 | .isRootRequired .label { 34 | @include foundation.label-required(); 35 | } 36 | 37 | // States 38 | .isRootStateInvalid { 39 | @include variants.validation(invalid); 40 | } 41 | 42 | .isRootStateValid { 43 | @include variants.validation(valid); 44 | } 45 | 46 | .isRootStateWarning { 47 | @include variants.validation(warning); 48 | } 49 | 50 | // Invisible label 51 | .isLabelHidden { 52 | @include accessibility.hide-text(); 53 | } 54 | 55 | // Layouts 56 | .hasRootLabelBefore { 57 | @include inline-field-layout.has-label-before(); 58 | } 59 | 60 | .isRootInFormLayout { 61 | @include inline-field-layout.in-form-layout(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | export { default as Toggle } from './Toggle'; 2 | -------------------------------------------------------------------------------- /src/components/Toolbar/Toolbar.module.scss: -------------------------------------------------------------------------------- 1 | // 1. Get rid of unwanted spacing of inline elements by invocation of flex layout. 2 | 3 | @use "../../styles/tools/spacing"; 4 | @use "theme"; 5 | 6 | @layer components.toolbar { 7 | .toolbar, 8 | .group { 9 | display: flex; 10 | flex-wrap: wrap; 11 | gap: theme.$gap; 12 | } 13 | 14 | .toolbar { 15 | @include spacing.bottom(layouts); 16 | } 17 | 18 | .item { 19 | display: flex; // 1. 20 | flex: none; 21 | flex-direction: column; // 1. 22 | } 23 | 24 | .isItemFlexible { 25 | flex: 1; 26 | min-width: 0; 27 | } 28 | 29 | .isToolbarAlignedToTop, 30 | .isGroupAlignedToTop { 31 | align-items: flex-start; 32 | } 33 | 34 | .isToolbarAlignedToMiddle, 35 | .isGroupAlignedToMiddle { 36 | align-items: center; 37 | } 38 | 39 | .isToolbarAlignedToBottom, 40 | .isGroupAlignedToBottom { 41 | align-items: flex-end; 42 | } 43 | 44 | .isToolbarAlignedToBaseline, 45 | .isGroupAlignedToBaseline { 46 | align-items: baseline; 47 | } 48 | 49 | .isToolbarJustifiedToStart, 50 | .isGroupJustifiedToStart { 51 | justify-content: flex-start; 52 | } 53 | 54 | .isToolbarJustifiedToCenter, 55 | .isGroupJustifiedToCenter { 56 | justify-content: center; 57 | } 58 | 59 | .isToolbarJustifiedToEnd, 60 | .isGroupJustifiedToEnd { 61 | justify-content: flex-end; 62 | } 63 | 64 | .isToolbarJustifiedToSpaceBetween, 65 | .isGroupJustifiedToSpaceBetween { 66 | justify-content: space-between; 67 | } 68 | 69 | .isToolbarDense, 70 | .isGroupDense, 71 | .isToolbarDense .group, 72 | .isGroupDense .group { 73 | gap: theme.$gap-dense; 74 | } 75 | 76 | .isToolbarNowrap, 77 | .isGroupNowrap { 78 | flex-wrap: nowrap; 79 | } 80 | 81 | .isToolbarNowrap > .item:not(.isItemFlexible), 82 | .isGroupNowrap > .item:not(.isItemFlexible) { 83 | flex: 0 1 auto; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/Toolbar/ToolbarGroup.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withGlobalProps } from '../../providers/globalProps'; 4 | import { classNames } from '../../helpers/classNames/classNames'; 5 | import { transferProps } from '../../helpers/transferProps'; 6 | import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; 7 | import { getAlignClassName } from './_helpers/getAlignClassName'; 8 | import styles from './Toolbar.module.scss'; 9 | 10 | export const ToolbarGroup = ({ 11 | align, 12 | children, 13 | dense, 14 | nowrap, 15 | ...restProps 16 | }) => { 17 | if (isChildrenEmpty(children)) { 18 | return null; 19 | } 20 | 21 | return ( 22 |
31 | {children} 32 |
33 | ); 34 | }; 35 | 36 | ToolbarGroup.defaultProps = { 37 | align: 'top', 38 | children: null, 39 | dense: false, 40 | nowrap: false, 41 | }; 42 | 43 | ToolbarGroup.propTypes = { 44 | /** 45 | * Vertical alignment of toolbar items in the group. 46 | */ 47 | align: PropTypes.oneOf(['top', 'middle', 'bottom', 'baseline']), 48 | /** 49 | * Grouped ToolbarItems. If none are provided nothing is rendered. 50 | */ 51 | children: PropTypes.node, 52 | /** 53 | * If `true`, spacing of toolbar items in the group will be reduced. 54 | */ 55 | dense: PropTypes.bool, 56 | /** 57 | * If set, the toolbar group will not wrap. 58 | */ 59 | nowrap: PropTypes.bool, 60 | }; 61 | 62 | export const ToolbarGroupWithGlobalProps = withGlobalProps(ToolbarGroup, 'ToolbarGroup'); 63 | 64 | export default ToolbarGroupWithGlobalProps; 65 | -------------------------------------------------------------------------------- /src/components/Toolbar/ToolbarItem.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { classNames } from '../../helpers/classNames/classNames'; 4 | import { transferProps } from '../../helpers/transferProps'; 5 | import { withGlobalProps } from '../../providers/globalProps'; 6 | import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; 7 | import styles from './Toolbar.module.scss'; 8 | 9 | export const ToolbarItem = ({ 10 | children, 11 | flexible, 12 | ...restProps 13 | }) => { 14 | if (isChildrenEmpty(children)) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
26 | {children} 27 |
28 | ); 29 | }; 30 | 31 | ToolbarItem.defaultProps = { 32 | children: null, 33 | flexible: false, 34 | }; 35 | 36 | ToolbarItem.propTypes = { 37 | /** 38 | * Content of the toolbar item. If none are provided nothing is rendered. 39 | */ 40 | children: PropTypes.node, 41 | /** 42 | * Allow item to grow and shrink if needed. 43 | */ 44 | flexible: PropTypes.bool, 45 | }; 46 | 47 | export const ToolbarItemWithGlobalProps = withGlobalProps(ToolbarItem, 'ToolbarItem'); 48 | 49 | export default ToolbarItemWithGlobalProps; 50 | -------------------------------------------------------------------------------- /src/components/Toolbar/__tests__/Toolbar.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { alignPropTest } from '../../../../tests/propTests/alignPropTest'; 7 | import { childrenEmptyPropTest } from '../../../../tests/propTests/childrenEmptyPropTest'; 8 | import { densePropTest } from '../../../../tests/propTests/densePropTest'; 9 | import { noWrapPropTest } from '../../../../tests/propTests/noWrapPropTest'; 10 | import { justifyPropTest } from '../../../../tests/propTests/justifyPropTest'; 11 | import { Toolbar } from '../Toolbar'; 12 | 13 | const defaultProps = { 14 | children:
other content text
, 15 | }; 16 | 17 | describe('rendering', () => { 18 | it.each([ 19 | ...alignPropTest('Toolbar'), 20 | ...childrenEmptyPropTest, 21 | [ 22 | { children:
other content text
}, 23 | (rootElement) => expect(within(rootElement).getByText('other content text')), 24 | ], 25 | ...densePropTest('Toolbar'), 26 | ...justifyPropTest('Toolbar'), 27 | ...noWrapPropTest('Toolbar'), 28 | ])('renders with props: "%s"', (testedProps, assert) => { 29 | const dom = render(( 30 | 34 | )); 35 | 36 | assert(dom.container.firstChild); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Toolbar/__tests__/ToolbarGroup.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { alignPropTest } from '../../../../tests/propTests/alignPropTest'; 7 | import { childrenEmptyPropTest } from '../../../../tests/propTests/childrenEmptyPropTest'; 8 | import { densePropTest } from '../../../../tests/propTests/densePropTest'; 9 | import { noWrapPropTest } from '../../../../tests/propTests/noWrapPropTest'; 10 | import { ToolbarGroup } from '../ToolbarGroup'; 11 | 12 | const defaultProps = { 13 | children:
other content text
, 14 | }; 15 | 16 | describe('rendering', () => { 17 | it.each([ 18 | ...alignPropTest('Group'), 19 | ...childrenEmptyPropTest, 20 | [ 21 | { children:
other content text
}, 22 | (rootElement) => expect(within(rootElement).getByText('other content text')), 23 | ], 24 | ...densePropTest('Group'), 25 | ...noWrapPropTest('Group'), 26 | ])('renders with props: "%s"', (testedProps, assert) => { 27 | const dom = render(( 28 | 32 | )); 33 | 34 | assert(dom.container.firstChild); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Toolbar/__tests__/ToolbarItem.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { childrenEmptyPropTest } from '../../../../tests/propTests/childrenEmptyPropTest'; 7 | import { ToolbarItem } from '../ToolbarItem'; 8 | 9 | const defaultProps = { 10 | children: 'content', 11 | }; 12 | 13 | describe('rendering', () => { 14 | it.each([ 15 | ...childrenEmptyPropTest, 16 | [ 17 | { children:
content text
}, 18 | (rootElement) => expect(within(rootElement).getByText('content text')), 19 | ], 20 | [ 21 | { 22 | children:
content text
, 23 | flexible: true, 24 | }, 25 | (rootElement) => expect(rootElement).toHaveClass('isItemFlexible'), 26 | ], 27 | ])('renders with props: "%s"', (testedProps, assert) => { 28 | const dom = render(( 29 | 33 | )); 34 | 35 | assert(dom.container.firstChild); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/Toolbar/_helpers/getAlignClassName.js: -------------------------------------------------------------------------------- 1 | export const getAlignClassName = (value, styles, type) => { 2 | if (value === 'top') { 3 | if (type === 'group') { 4 | return styles.isGroupAlignedToTop; 5 | } 6 | return styles.isToolbarAlignedToTop; 7 | } 8 | 9 | if (value === 'middle') { 10 | if (type === 'group') { 11 | return styles.isGroupAlignedToMiddle; 12 | } 13 | return styles.isToolbarAlignedToMiddle; 14 | } 15 | 16 | if (value === 'bottom') { 17 | if (type === 'group') { 18 | return styles.isGroupAlignedToBottom; 19 | } 20 | return styles.isToolbarAlignedToBottom; 21 | } 22 | 23 | if (type === 'group') { 24 | return styles.isGroupAlignedToBaseline; 25 | } 26 | return styles.isToolbarAlignedToBaseline; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Toolbar/_helpers/getJustifyClassName.js: -------------------------------------------------------------------------------- 1 | export const getJustifyClassName = (value, styles) => { 2 | if (value === 'start') { 3 | return styles.isToolbarJustifiedToStart; 4 | } 5 | 6 | if (value === 'center') { 7 | return styles.isToolbarJustifiedToCenter; 8 | } 9 | 10 | if (value === 'end') { 11 | return styles.isToolbarJustifiedToEnd; 12 | } 13 | 14 | return styles.isToolbarJustifiedToSpaceBetween; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /src/components/Toolbar/_theme.scss: -------------------------------------------------------------------------------- 1 | $gap: var(--rui-Toolbar__gap); 2 | $gap-dense: var(--rui-Toolbar__gap--dense); 3 | -------------------------------------------------------------------------------- /src/components/Toolbar/index.js: -------------------------------------------------------------------------------- 1 | export { default as Toolbar } from './Toolbar'; 2 | export { default as ToolbarGroup } from './ToolbarGroup'; 3 | export { default as ToolbarItem } from './ToolbarItem'; 4 | -------------------------------------------------------------------------------- /src/components/_helpers/getRootColorClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootColorClassName = (variant, styles) => { 2 | if (variant === 'primary') { 3 | return styles.isRootColorPrimary; 4 | } 5 | 6 | if (variant === 'secondary') { 7 | return styles.isRootColorSecondary; 8 | } 9 | 10 | if (variant === 'selected') { 11 | return styles.isRootColorSelected; 12 | } 13 | 14 | if (variant === 'success') { 15 | return styles.isRootColorSuccess; 16 | } 17 | 18 | if (variant === 'warning') { 19 | return styles.isRootColorWarning; 20 | } 21 | 22 | if (variant === 'danger') { 23 | return styles.isRootColorDanger; 24 | } 25 | 26 | if (variant === 'help') { 27 | return styles.isRootColorHelp; 28 | } 29 | 30 | if (variant === 'info') { 31 | return styles.isRootColorInfo; 32 | } 33 | 34 | if (variant === 'note') { 35 | return styles.isRootColorNote; 36 | } 37 | 38 | if (variant === 'light') { 39 | return styles.isRootColorLight; 40 | } 41 | 42 | if (variant === 'dark') { 43 | return styles.isRootColorDark; 44 | } 45 | 46 | return null; 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/_helpers/getRootPriorityClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootPriorityClassName = (priority, styles) => { 2 | if (priority === 'filled') { 3 | return styles.isRootPriorityFilled; 4 | } 5 | 6 | if (priority === 'outline') { 7 | return styles.isRootPriorityOutline; 8 | } 9 | 10 | if (priority === 'flat') { 11 | return styles.isRootPriorityFlat; 12 | } 13 | 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/_helpers/getRootSizeClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootSizeClassName = (size, styles) => { 2 | if (size === 'small') { 3 | return styles.isRootSizeSmall; 4 | } 5 | 6 | if (size === 'medium') { 7 | return styles.isRootSizeMedium; 8 | } 9 | 10 | if (size === 'large') { 11 | return styles.isRootSizeLarge; 12 | } 13 | 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/_helpers/getRootValidationStateClassName.js: -------------------------------------------------------------------------------- 1 | export const getRootValidationStateClassName = (validationState, styles) => { 2 | if (validationState === 'invalid') { 3 | return styles.isRootStateInvalid; 4 | } 5 | 6 | if (validationState === 'valid') { 7 | return styles.isRootStateValid; 8 | } 9 | 10 | if (validationState === 'warning') { 11 | return styles.isRootStateWarning; 12 | } 13 | 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/_helpers/isChildrenEmpty.js: -------------------------------------------------------------------------------- 1 | export const isChildrenEmpty = (children) => children == null 2 | || children === false 3 | || (Array.isArray(children) && children.length === 0); 4 | -------------------------------------------------------------------------------- /src/components/_helpers/resolveContextOrProp.js: -------------------------------------------------------------------------------- 1 | export const resolveContextOrProp = (contextValue, propValue) => { 2 | // We need to test: 3 | // * `false` - for when the `contextValue` is boolean 4 | // * `null` - for when the `contextValue` is non-boolean 5 | if (contextValue === false || contextValue === null) { 6 | return propValue; 7 | } 8 | 9 | return contextValue; 10 | }; 11 | -------------------------------------------------------------------------------- /src/docs/_assets/js/ruiSwatch.js: -------------------------------------------------------------------------------- 1 | class RuiSwatch extends HTMLElement { 2 | async connectedCallback() { 3 | const color = this.attributes.color.value; 4 | const colorCustomProperty = `--rui-color-${color}`; 5 | 6 | const rootEl = document.createElement('div'); 7 | rootEl.style.display = 'inline-flex'; 8 | rootEl.style['flex-direction'] = 'column'; 9 | rootEl.style.width = '18%'; 10 | rootEl.style['min-width'] = '120px'; 11 | rootEl.style['margin-right'] = '1rem'; 12 | rootEl.style['margin-bottom'] = '1.5rem'; 13 | 14 | const swatchEl = document.createElement('div'); 15 | swatchEl.style.backgroundColor = `var(${colorCustomProperty})`; 16 | swatchEl.style.height = '5rem'; 17 | swatchEl.style['margin-bottom'] = '0.5rem'; 18 | swatchEl.style.border = '1px solid rgb(0 0 0 / 15%)'; 19 | 20 | const titleEl = document.createElement('div'); 21 | titleEl.style.width = '100%'; 22 | titleEl.style.overflow = 'hidden'; 23 | titleEl.style['font-size'] = '0.85rem'; 24 | titleEl.style['text-overflow'] = 'ellipsis'; 25 | 26 | const label = document.createElement('strong'); 27 | label.style.display = 'block'; 28 | label.innerText = color; 29 | titleEl.appendChild(label); 30 | 31 | const code = document.createElement('code'); 32 | code.innerText = colorCustomProperty; 33 | titleEl.appendChild(code); 34 | 35 | rootEl.appendChild(swatchEl); 36 | rootEl.appendChild(titleEl); 37 | this.append(rootEl); 38 | } 39 | } 40 | customElements.define('rui-swatch', RuiSwatch); 41 | -------------------------------------------------------------------------------- /src/docs/_assets/racom.svg: -------------------------------------------------------------------------------- 1 | 2 | RACOM 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/_overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block site_meta %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /src/docs/contribute/api.md: -------------------------------------------------------------------------------- 1 | # API Guidelines 2 | 3 | Guiding principles for designing component APIs. 4 | 5 | ## Visual Props: API vs. Theme 6 | 7 | While a custom theme is intended to define visual and interaction settings, 8 | component API can be designed to allow customization of many kinds, including 9 | the visual properties. This may be confusing during the design process of a 10 | component. So how do you reliably know which approach you need? 11 | 12 | The key difference is whether you want to enable overriding the default value in 13 | the component instance: 14 | 15 | > Do I want to allow overriding this particular visual setting in a component 16 | instance? 17 | 18 | - If yes, put it into the API of the component. Developers can adjust their 19 | [global props](/docs/customize/global-props), but the option value can still be 20 | overridden per component instance. 21 | - If not, put it into the theme. Developers can change it 22 | [in their theme](/docs/customize/theming/overview) and it will be 23 | the same for all component instances. 24 | 25 | ## Measures 26 | 27 | Always use [spacing values](/docs/foundation/spacing) for all kinds of measures like 28 | offsets, gaps, or spacings. This helps keep the design consistent across 29 | components. 30 | -------------------------------------------------------------------------------- /src/docs/contribute/composition.md: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | There are several types of composition approaches. 4 | 5 | 1. **Self-contained components:** just a single component is needed. E.g. 6 | `Alert`, `Button`, `TextField`, etc. 7 | 8 | 2. **Components with subcomponents:** subcomponents cannot exist on their own 9 | outside their parent components. 10 | 11 | - **Mandatory subcomponents:** subcomponent must be used at least once in 12 | order for the composition to work. E.g. `Tabs` + `TabsItem`. 13 | 14 | - **Optional subcomponents:** optional subcomponents may be used to achieve 15 | special results. E.g. `FormLayout` + `FormLayoutCustomField` or `Grid` + 16 | `GridSpan`. 17 | 18 | - **Both mandatory and optional subcomponents:** e.g. `Card` + `CardBody` 19 | (mandatory) + `CardFooter` (optional), `Toolbar` + `ToolbarItem` 20 | (mandatory) + `ToolbarGroup` (optional), etc. 21 | 22 | 3. **Wrappers for other components:** component is designed to wrap other 23 | self-contained components. E.g. `FormLayout` + form fields (`CheckboxField`, 24 | `TextField`, `Toggle`, …) or `ButtonGroup` + `Button`. 25 | 26 | While authoring self-contained components (1) and wrappers (3) is quite 27 | straightforward, components with subcomponents require special attention when 28 | styling. Head to [CSS Guidelines] to learn more. 29 | 30 | [CSS Guidelines]: /docs/contribute/css 31 | -------------------------------------------------------------------------------- /src/docs/contribute/general-guidelines.md: -------------------------------------------------------------------------------- 1 | ../../../CONTRIBUTING.md -------------------------------------------------------------------------------- /src/docs/contribute/releasing.md: -------------------------------------------------------------------------------- 1 | ../../../RELEASING.md -------------------------------------------------------------------------------- /src/docs/css-helpers/animation.md: -------------------------------------------------------------------------------- 1 | # Animation 2 | 3 | The animation helper allows applying simple animations to UI elements. 4 | 5 | 👉 Remember that non-block inline elements (ie. an unstyled `span` or elements 6 | that have `display: inline`) cannot be animated. 7 | 8 | ```docoff-react-preview 9 |

10 | .animation-spin-clockwise 11 | 12 | 13 | 14 |

15 |

16 | .animation-spin-counterclockwise 17 | 18 | 19 | 20 |

21 | ``` 22 | -------------------------------------------------------------------------------- /src/docs/customize/font.md: -------------------------------------------------------------------------------- 1 | # Font 2 | 3 | React UI uses [native font stack][sm-native-font-stack] for optimum text 4 | rendering on every device and OS. 5 | 6 | This is a good practice because it reduces the size of the data transferred, and 7 | it also ensures that the text is displayed in the font that the user is most 8 | comfortable with. 9 | 10 | You can change it to a custom font by loading the font in your project: 11 | 12 | ```html 13 | 17 | ``` 18 | 19 | … and [overriding](/docs/customize/theming/overview) the 20 | `--rui-font-family-base` CSS custom property: 21 | 22 | ```css 23 | :root { 24 | --rui-font-family-base: 'Titillium Web', helvetica, roboto, arial, sans-serif; 25 | } 26 | ``` 27 | 28 | [sm-native-font-stack]: https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ 29 | -------------------------------------------------------------------------------- /src/docs/customize/translations.md: -------------------------------------------------------------------------------- 1 | # Translations 2 | 3 | Some components may contain texts which improve components' accessibility. 4 | All texts are in English by default and can be translated to other languages. 5 | 6 | Structure of translations can be found in the file [src/translations/en.json]. 7 | 8 | To use custom translations, you need to import `TranslationsProvider` first: 9 | 10 | ```js 11 | import { TranslationsProvider } from '@react-ui-org/react-ui'; 12 | ``` 13 | 14 | Then wrap application (or its part) with `TranslationsProvider` with 15 | the `translations` prop object. 16 | 17 | ```docoff-react-preview 18 | 21 | alert('You closed me!')}> 22 | Hi, I'm a closable Alert. 23 | 24 | 25 | ``` 26 | 27 | ## Nesting 28 | 29 | The `TranslationsProvider`s can be nested. This is useful e.g. when you want to 30 | configure translations across whole application and then override some of them 31 | in a specific part of the application. 32 | 33 | When a nested `TranslationsProvider` is used, the props are merged deeply together. 34 | This means that you can extend specific object with new props or override existing 35 | ones. 36 | 37 | ```docoff-react-preview 38 | 41 | alert('You closed me!')}> 42 | Hi, I'm a closable Alert. 43 | 44 | 45 | 48 | alert('You closed me!')}> 49 | Hi, I'm another Alert and I'm also closable. 50 | 51 | 52 | 53 | ``` 54 | 55 | [src/translations/en.json]: https://github.com/react-ui-org/react-ui/blob/master/src/translations/en.js 56 | -------------------------------------------------------------------------------- /src/docs/foundation/borders.md: -------------------------------------------------------------------------------- 1 | # Borders 2 | 3 | Borders separate the content from the outer context. 4 | 5 | 👉 All values on this page can be changed by 6 | [overriding](/docs/customize/theming/overview) values in your 7 | [design tokens](/docs/foundation/design-tokens). 8 | 9 | Available border widths (the list may grow in the future): 10 | 11 | | Border width | Value | Usage in CSS | Purpose | 12 | |--------------|-------|----------------------------------|--------------------| 13 | | 1 | 1 px | `--rui-dimension-border-width-1` | Base border width | 14 | 15 | Available border styles: 16 | 17 | | Border style | Usage in CSS | Purpose | 18 | |--------------|---------------------------|-----------------------------------| 19 | | focus ring | `--rui-border-focus-ring` | Focus ring style | 20 | 21 | 👉 Check [Colors](/docs/foundation/colors#border-colors) for available border colors. 22 | 23 | 👉 Check [Accessibility](/docs/foundation/accessibility#keyboard-friendliness) for 24 | all focus ring options. 25 | -------------------------------------------------------------------------------- /src/docs/foundation/breakpoints.md: -------------------------------------------------------------------------------- 1 | # Breakpoints 2 | 3 | There are 7 CSS breakpoints to provide you maximum control over the 4 | responsive behavior of the layout of your app. 5 | 6 | These breakpoint values are used throughout React UI, in components, or in 7 | helper classes. You can reuse them in your own CSS and components to create a 8 | seamless experience for your users. 9 | 10 | | Name | Value in em | Value in px* | Usage in CSS** | Usage in SCSS | 11 | |------|------------:|-------------:|-----------------------------------|--------------------------------| 12 | | xs | 0 em | 0 px | `--rui-dimension-breakpoint-xs` | `@include breakpoint.up(xs)` | 13 | | sm | 36 em | 576 px | `--rui-dimension-breakpoint-sm` | `@include breakpoint.up(sm)` | 14 | | md | 48 em | 768 px | `--rui-dimension-breakpoint-md` | `@include breakpoint.up(md)` | 15 | | lg | 66 em | 1056 px | `--rui-dimension-breakpoint-lg` | `@include breakpoint.up(lg)` | 16 | | xl | 84 em | 1344 px | `--rui-dimension-breakpoint-xl` | `@include breakpoint.up(xl)` | 17 | | x2l | 90 em | 1440 px | `--rui-dimension-breakpoint-x2l` | `@include breakpoint.up(x2l)` | 18 | | x3l | 120 em | 1920 px | `--rui-dimension-breakpoint-x3l` | `@include breakpoint.up(x3l)` | 19 | 20 | \* Supposed the root font size is 16 px. 21 | 22 | \** ⚠️ Consider **CSS breakpoints as read-only:** because 23 | [CSS custom properties][w3-custom-properties] cannot be used inside media 24 | queries (media query is [not a CSS property][so-custom-properties]), changing 25 | their values will have no effect. If you need to adjust the breakpoint values, 26 | you must override the `$values` SCSS map defined in 27 | `styles/settings/_breakpoints.scss`. 28 | 29 | [w3-custom-properties]: https://www.w3.org/TR/css-variables-1/#using-variables 30 | [so-custom-properties]: https://stackoverflow.com/q/40722882 31 | -------------------------------------------------------------------------------- /src/docs/foundation/collections.md: -------------------------------------------------------------------------------- 1 | # Collections 2 | 3 | Collections are lists of available values that can be used to customize the 4 | appearance of components, such as colors, sizes, and placement. Collections are 5 | used to ensure consistency across the design system. 6 | 7 | ## General Guidelines 8 | 9 | - Components can support one or more collections from a collection category. 10 | Refer to the documentation for each component to see which collections are 11 | available. 12 | - If an option from a collection is used in a component, all other options from 13 | the same collection must be available for use in that component too. 14 | 15 | ## Colors 16 | 17 | The following color names are designed for use in components that support the 18 | `color` prop: 19 | 20 | | Collection | Available values | Description | 21 | |------------|--------------------------------------------------------|------------------------------------------------------------------------------------| 22 | | Action | `primary`, `secondary`, `selected` | Reserved for actionable elements, such as buttons and navigation links | 23 | | Feedback | `success`, `warning`, `danger`, `info`, `help`, `note` | For components with feedback state, such as alerts and buttons | 24 | | Neutral | `light`, `dark` | For components that require a neutral background color, such as badges and buttons | 25 | -------------------------------------------------------------------------------- /src/docs/foundation/design-tokens.md: -------------------------------------------------------------------------------- 1 | # Design Tokens 2 | 3 | Design tokens are a [methodology] for **expressing design decisions** in a 4 | platform-agnostic way so that they can be shared across different disciplines, 5 | tools, and technologies. They help establish a common vocabulary across 6 | organizations. 7 | 8 | 👉 Design tokens are your starting point for 9 | [customization](/docs/customize/theming/overview) of React UI to make it fit your 10 | design system needs. React UI uses CSS custom properties as a primary storage 11 | format for design tokens. 12 | 13 | ## Global Tokens 14 | 15 | Global tokens represent the basic, context-agnostic values in your design 16 | language. They define color palettes, typography scales, or spacing values, 17 | without binding them to any semantic meaning. 18 | 19 | ```css 20 | :root { 21 | --pantone-3145c: #00778b; 22 | } 23 | ``` 24 | 25 | ## Semantic Tokens 26 | 27 | Semantic tokens define roles and decisions that give the design system its 28 | character. They communicate the intended purpose of a global token and are often 29 | reused by component tokens. 30 | 31 | ```css 32 | :root { 33 | --rui-color-action-primary: var(--pantone-3145c); 34 | } 35 | ``` 36 | 37 | ## Component Tokens 38 | 39 | Component tokens represent the values associated with a component. They often 40 | inherit from semantic tokens, but are named in a way that narrows down their 41 | reusability to the context of the specific component. 42 | 43 | ```css 44 | :root { 45 | --rui-Button--filled--primary--default__background: var(--rui-color-action-primary); 46 | } 47 | ``` 48 | 49 | [methodology]: https://uxdesign.cc/design-tokens-for-dummies-8acebf010d71 50 | -------------------------------------------------------------------------------- /src/docs/foundation/icons.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | React UI **does not include any icons** to allow more flexibility and to reduce 4 | its size. Components that require icons allow them to be passed in via props. 5 | 6 | Example: 7 | 8 | ```jsx 9 | import { Alert } from '@react-ui-org/react-ui'; 10 | import { Icon } from './my-icon-component'; 11 | 12 | } 14 | color="success" 15 | > 16 | This is a success! 17 | 18 | ``` 19 | -------------------------------------------------------------------------------- /src/docs/foundation/shadows.md: -------------------------------------------------------------------------------- 1 | # Shadows 2 | 3 | Use shadows to add depth to your components and to better separate them from 4 | background. 5 | 6 | 👉 All values on this page can be changed by 7 | [overriding](/docs/customize/theming/overview) values in your 8 | [design tokens](/docs/foundation/design-tokens). 9 | 10 | | Shadow | Usage in CSS | Purpose | 11 | |------------|---------------------------|------------------------------------------| 12 | | layer 1 | `--rui-shadow-layer-1` | Elevation, level 1 | 13 | | layer 2 | `--rui-shadow-layer-2` | Elevation, level 2 | 14 | | focus ring | `--rui-shadow-focus-ring` | Focus ring to be used instead of outline | 15 | 16 | 👉 Check how the layer shadows pair nicely with their 17 | [background color counterparts](/docs/foundation/colors#content-layers). 18 | 19 | 👉 Check [Accessibility](/docs/foundation/accessibility#keyboard-friendliness) for 20 | all focus ring options. 21 | -------------------------------------------------------------------------------- /src/docs/getting-started/browsers-and-devices.md: -------------------------------------------------------------------------------- 1 | # Browsers & Devices 2 | 3 | React UI supports the **latest, stable releases** of major browsers and 4 | platforms. 5 | 6 | Alternative browsers which use the latest version of WebKit, Blink, or Gecko, 7 | whether directly or via the platform's web view API, are not explicitly 8 | supported. However, React UI should (in most cases) display and function 9 | correctly in these browsers as well. 10 | 11 | We use [Autoprefixer](https://autoprefixer.github.io) and 12 | [Babel](https://babeljs.io) to handle intended browser support, which use 13 | [Browserslist](https://github.com/browserslist/browserslist) to manage these 14 | browser versions. See the source of our `.browserslistrc` config to learn how. 15 | 16 | ## Mobile Devices 17 | 18 | | | Chrome | Firefox | Safari | 19 | |---------|--------|---------|--------| 20 | | Android | ✅ | ✅ | — | 21 | | iOS | ✅ | ✅ | ✅ | 22 | 23 | ## Desktop Browsers 24 | 25 | | | Chrome | Firefox | Safari | Microsoft Edge | 26 | |---------|--------|---------|--------|----------------| 27 | | Windows | ✅ | ✅ | — | ✅ | 28 | | macOS | ✅ | ✅ | ✅ | ✅ | 29 | | Linux | ✅ | ✅ | — | — | 30 | 31 | To be explicit: Internet Explorer is not supported. 32 | -------------------------------------------------------------------------------- /src/docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | React UI is distributed as a npm package. To add it to your project, run: 4 | 5 | ```bash 6 | npm install --save @react-ui-org/react-ui 7 | ``` 8 | 9 | Please note that `prop-types`, `react` and `react-dom` are peer dependencies. 10 | -------------------------------------------------------------------------------- /src/docsCustomProperties.js: -------------------------------------------------------------------------------- 1 | // This is needed to generate CSS file containing only CSS custom property definitions 2 | // This file is needed to make custom property values accessible in the documentation 3 | import './theme.scss'; 4 | -------------------------------------------------------------------------------- /src/foundation.scss: -------------------------------------------------------------------------------- 1 | // Mandatory themeable CSS layer to prepare ground for components. 2 | 3 | @use "sass:meta"; 4 | 5 | // 6 | // Generic 7 | // ======= 8 | // 9 | // Ground-zero styles. 10 | 11 | @layer foundation.generic { 12 | @include meta.load-css("styles/generic/box-sizing"); 13 | @include meta.load-css("normalize.css/normalize.css"); 14 | @include meta.load-css("styles/generic/focus"); 15 | @include meta.load-css("styles/generic/forms"); 16 | @include meta.load-css("styles/generic/reset"); 17 | @include meta.load-css("styles/generic/shared"); 18 | } 19 | 20 | // 21 | // Elements 22 | // ======== 23 | // 24 | // Unclassed HTML elements (type selectors). 25 | 26 | @layer foundation.elements { 27 | @include meta.load-css("styles/elements/code"); 28 | @include meta.load-css("styles/elements/links"); 29 | @include meta.load-css("styles/elements/lists"); 30 | @include meta.load-css("styles/elements/page"); 31 | @include meta.load-css("styles/elements/rulers"); 32 | @include meta.load-css("styles/elements/small"); 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers.scss: -------------------------------------------------------------------------------- 1 | // Optional layer with helper CSS classes to easily adjust visual details. 2 | 3 | @use "sass:meta"; 4 | 5 | // 6 | // Helpers 7 | // ======= 8 | // 9 | // General purpose helpers for common situations. They can compose multiple CSS rules to do a bit 10 | // more complicated tasks. 11 | 12 | @layer helpers { 13 | @include meta.load-css("styles/helpers/animation"); 14 | } 15 | 16 | // 17 | // Utilities 18 | // ========= 19 | // 20 | // Utility classes to tweak small details like typography, margins or padding. They do just one 21 | // thing: they set a single CSS rule and use the otherwise disallowed `!important` to enforce it. 22 | // Also they are often responsive (can be adjusted for individual breakpoints). 23 | 24 | @layer utilities { 25 | @include meta.load-css("styles/utilities"); 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/classNames/README.md: -------------------------------------------------------------------------------- 1 | # classNames 2 | 3 | The `classNames` helper function simplifies creating a string passable to 4 | the `class` / `className` attribute. 5 | 6 | It accepts multiple arguments, filters out invalid values, and returns a single 7 | string where the remaining parameters are joined by a space. 8 | 9 | ## Usage 10 | 11 | To use `classNames` helper, you need to import it first: 12 | 13 | ```js 14 | import { classNames } from '@react-ui-org/react-ui'; 15 | ``` 16 | 17 | And use it: 18 | 19 | ```docoff-react-preview 20 | <> 21 |
27 | {(new Date()).toLocaleDateString()} 28 |
29 |
35 | {(new Date()).toLocaleDateString()} 36 |
37 | 38 | ``` 39 | 40 | ## Parameter Filtering 41 | 42 | The `classNames` function: 43 | 44 | * filters out all values that are not strings 45 | * filters out empty strings 46 | * filters out whitespace only strings 47 | 48 | 49 | ```docoff-react-preview 50 | {classNames( 51 | 'class-1', 52 | 'class-2 class-3', 53 | ' ', 54 | ' ', // non-breakable space 55 | ' ', // tab 56 | '', 57 | 0, 58 | 1, 59 | null, 60 | undefined, 61 | true, 62 | false, 63 | )} 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /src/helpers/classNames/__tests__/classNames.test.js: -------------------------------------------------------------------------------- 1 | import { classNames } from '../classNames'; 2 | 3 | describe('classNames', () => { 4 | it('returns filtered class names', () => { 5 | const result = classNames( 6 | 'class-1', 7 | 'class-2 class-3', 8 | ' ', 9 | ' ', // non=-breakable space 10 | ' ', // eslint-disable-line no-tabs 11 | '', 12 | 0, 13 | 1, 14 | null, 15 | undefined, 16 | true, 17 | false, 18 | ); 19 | 20 | expect(result).toEqual('class-1 class-2 class-3'); 21 | }); 22 | 23 | it('returns `undefined` if called with no params', () => { 24 | const result = classNames(); 25 | 26 | expect(result).toEqual(undefined); 27 | }); 28 | 29 | it('returns `undefined` if called with params that get all filtered out', () => { 30 | const result = classNames(false); 31 | 32 | expect(result).toEqual(undefined); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/helpers/classNames/classNames.js: -------------------------------------------------------------------------------- 1 | export const classNames = (...classes) => { 2 | const filteredClassNames = classes.filter( 3 | (className) => typeof className === 'string' 4 | && className.trim().length > 0, 5 | ); 6 | 7 | return filteredClassNames.length > 0 8 | ? filteredClassNames.join(' ') 9 | // React does not render attributes whose value is `undefined` and we do not want an empty `class` attribute in HTML 10 | : undefined; 11 | }; 12 | -------------------------------------------------------------------------------- /src/helpers/classNames/index.js: -------------------------------------------------------------------------------- 1 | export { classNames } from './classNames'; 2 | -------------------------------------------------------------------------------- /src/helpers/transferProps/README.md: -------------------------------------------------------------------------------- 1 | # transferProps 2 | 3 | The `transferProps` helper controls passing of props from the React component 4 | to the HTML element. 5 | 6 | It enables making the component interactive and helps to improve its 7 | accessibility. However some props should never be passed to the HTML element 8 | as it would break things. This function is used to filter them out. Among these 9 | props are: 10 | 11 | - `children` 12 | - `className` 13 | - `contentEditable` 14 | - `dangerouslySetInnerHtml` 15 | - `ref` 16 | - `staticContext` 17 | - `style` 18 | - `suppressContentEditableWarning` 19 | 20 | 👉 When run in development mode, the function will log the error to the console 21 | if any invalid props are passed. 22 | 23 | ## Basic Usage 24 | 25 | To use `transferProps` helper, you need to import it first: 26 | 27 | ```js 28 | import { transferProps } from "@react-ui-org/react-ui"; 29 | ``` 30 | 31 | And use it: 32 | 33 | ```jsx 34 | const CustomComponent = ({ 35 | children, 36 | id, 37 | ...restProps 38 | }) => ( 39 |
43 | {children} 44 |
45 | ); 46 | ``` 47 | -------------------------------------------------------------------------------- /src/helpers/transferProps/__tests__/transferProps.test.js: -------------------------------------------------------------------------------- 1 | import { transferProps } from '../transferProps'; 2 | 3 | describe('transferProps', () => { 4 | it('returns all props when always blacklisted props are not present', () => { 5 | const props = { 6 | propA: 'value', 7 | propB: 'value', 8 | }; 9 | const expectedProps = { ...props }; 10 | 11 | expect(transferProps(props)).toEqual(expectedProps); 12 | }); 13 | 14 | it('returns filtered props using always blacklisted props', () => { 15 | const props = { 16 | className: 'value', 17 | contentEditable: true, 18 | propA: 'value', 19 | }; 20 | const expectedProps = { propA: 'value' }; 21 | 22 | let errorString; 23 | // eslint-disable-next-line no-console 24 | const originalConsoleError = console.error; 25 | // eslint-disable-next-line no-console 26 | console.error = (error) => { 27 | errorString = error; 28 | }; 29 | expect(transferProps(props)).toEqual(expectedProps); 30 | expect(errorString).toEqual('Invalid prop(s) supplied to the "transferProps" function: "className", "contentEditable"'); 31 | 32 | // eslint-disable-next-line no-console 33 | console.error = originalConsoleError; 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/helpers/transferProps/index.js: -------------------------------------------------------------------------------- 1 | export { transferProps } from './transferProps'; 2 | -------------------------------------------------------------------------------- /src/helpers/transferProps/transferProps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param props The props that were passed to the React component and were not used by it 3 | * @returns The props to be passed to the HTML element 4 | */ 5 | export const transferProps = (props) => { 6 | const { 7 | children, 8 | className, 9 | contentEditable, 10 | dangerouslySetInnerHTML, 11 | ref, 12 | staticContext, 13 | style, 14 | suppressContentEditableWarning, 15 | ...restProps 16 | } = props; 17 | 18 | if (process.env.NODE_ENV !== 'production') { 19 | const invalidProps = [ 20 | 'children', // It is always either handled by the component itself or not supported. 21 | 'className', // Classes are set by component authors, changing it arbitrarily might break things. 22 | 'contentEditable', // Components are either interactive or not, changing it arbitrarily might break things. 23 | 'dangerouslySetInnerHTML', // This might break things and allow for XSS attacks. 24 | 'ref', // Forwarding `ref` is hardcoded and documented for each component. 25 | 'staticContext', // In `react-router` (v4, v5) this is used during server side rendering, it makes no sense to pass it to a component. 26 | 'style', // Styles are set by component authors, changing it arbitrarily might break things. 27 | 'suppressContentEditableWarning', // Since setting `contentEditable` is not allowed, this is not needed. 28 | ] 29 | .filter((key) => props[key] !== undefined); 30 | 31 | if (invalidProps.length > 0) { 32 | // eslint-disable-next-line no-console 33 | console.error(`Invalid prop(s) supplied to the "transferProps" function: "${invalidProps.join('", "')}"`); 34 | } 35 | } 36 | 37 | return restProps; 38 | }; 39 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | // First establish cascade layers: 2 | @forward "layers"; // ⚠️ Must come first for the cascade layers to work as intended. 3 | 4 | // Then import the rest of the files, already organized by layer: 5 | @forward "theme"; 6 | @forward "foundation"; 7 | @forward "helpers"; 8 | -------------------------------------------------------------------------------- /src/layers.scss: -------------------------------------------------------------------------------- 1 | // Establish CSS cascade layers. 2 | // ⚠️ WARNING: This file must be called before other React UI styles for the cascade layers to work as intended. 3 | 4 | @layer theme, foundation, helpers, components, utilities; 5 | -------------------------------------------------------------------------------- /src/providers/globalProps/GlobalPropsContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GlobalPropsContext = React.createContext({}); 4 | 5 | export default GlobalPropsContext; 6 | -------------------------------------------------------------------------------- /src/providers/globalProps/GlobalPropsProvider.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { 3 | useContext, 4 | } from 'react'; 5 | import { mergeDeep } from '../../utils/mergeDeep'; 6 | import GlobalPropsContext from './GlobalPropsContext'; 7 | 8 | const GlobalPropsProvider = ({ 9 | children, 10 | globalProps, 11 | }) => { 12 | const contextGlobalProps = useContext(GlobalPropsContext); 13 | 14 | return ( 15 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | GlobalPropsProvider.defaultProps = { 24 | children: null, 25 | globalProps: {}, 26 | }; 27 | 28 | GlobalPropsProvider.propTypes = { 29 | children: PropTypes.node, 30 | globalProps: PropTypes.shape({}), 31 | }; 32 | 33 | export default GlobalPropsProvider; 34 | -------------------------------------------------------------------------------- /src/providers/globalProps/index.js: -------------------------------------------------------------------------------- 1 | export { default as GlobalPropsContext } from './GlobalPropsContext'; 2 | export { default as GlobalPropsProvider } from './GlobalPropsProvider'; 3 | export { default as withGlobalProps } from './withGlobalProps'; 4 | -------------------------------------------------------------------------------- /src/providers/globalProps/withGlobalProps.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { 3 | useContext, 4 | } from 'react'; 5 | import GlobalPropsContext from './GlobalPropsContext'; 6 | 7 | export default (Component, componentName) => { 8 | const WithGlobalPropsComponent = ({ 9 | forwardedRef, 10 | ...rest 11 | }) => { 12 | const contextGlobalProps = useContext(GlobalPropsContext); 13 | 14 | return ( 15 | 20 | ); 21 | }; 22 | 23 | WithGlobalPropsComponent.defaultProps = { 24 | forwardedRef: undefined, 25 | }; 26 | 27 | WithGlobalPropsComponent.propTypes = { 28 | forwardedRef: PropTypes.oneOfType([ 29 | PropTypes.func, 30 | 31 | // The props can be of any type and here we need to support them all 32 | // eslint-disable-next-line react/forbid-prop-types 33 | PropTypes.shape({ current: PropTypes.any }), 34 | ]), 35 | }; 36 | 37 | return React.forwardRef((props, ref) => ()); 38 | }; 39 | -------------------------------------------------------------------------------- /src/providers/translations/TranslationsContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import defaultTranslations from '../../translations/en'; 3 | 4 | const RUIContext = React.createContext(defaultTranslations); 5 | 6 | export default RUIContext; 7 | -------------------------------------------------------------------------------- /src/providers/translations/TranslationsProvider.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { 3 | useContext, 4 | } from 'react'; 5 | import { mergeDeep } from '../../utils/mergeDeep'; 6 | import TranslationsContext from './TranslationsContext'; 7 | 8 | const TranslationsProvider = ({ 9 | children, 10 | translations, 11 | }) => { 12 | const contextTranslations = useContext(TranslationsContext); 13 | 14 | return ( 15 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | TranslationsProvider.defaultProps = { 24 | children: null, 25 | translations: {}, 26 | }; 27 | 28 | TranslationsProvider.propTypes = { 29 | children: PropTypes.node, 30 | translations: PropTypes.shape({}), 31 | }; 32 | 33 | export default TranslationsProvider; 34 | -------------------------------------------------------------------------------- /src/providers/translations/__tests__/TranslationsProvider.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | within, 5 | } from '@testing-library/react'; 6 | import { Alert } from '../../../components/Alert'; 7 | import { ScrollView } from '../../../components/ScrollView'; 8 | import TranslationsProvider from '../TranslationsProvider'; 9 | 10 | describe('rendering', () => { 11 | it('renders with translations', () => { 12 | const dom = render(( 13 | 18 | {}}>alert text 19 | 20 | 21 | )); 22 | 23 | expect(within(dom.container.firstChild).getByTitle('Zavřít')); 24 | }); 25 | 26 | it('renders with nested translations', () => { 27 | const dom = render(( 28 | 36 | 41 | some scrolable text 42 | 43 | 44 | 45 | )); 46 | 47 | expect(within(dom.container.firstChild).getByTitle('Předchozí')); 48 | expect(within(dom.container.firstChild).getByTitle('Siguiente')); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/providers/translations/index.js: -------------------------------------------------------------------------------- 1 | export { default as TranslationsContext } from './TranslationsContext'; 2 | export { default as TranslationsProvider } from './TranslationsProvider'; 3 | -------------------------------------------------------------------------------- /src/styles/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "sass:meta"; 3 | @use "settings/breakpoints" as breakpoint-settings; 4 | @use "settings/utilities" as utility-settings; 5 | @use "tools/breakpoint"; 6 | @use "tools/utilities"; 7 | 8 | @each $breakpoint in map.keys(breakpoint-settings.$values) { 9 | @include breakpoint.up($breakpoint) { 10 | $infix: if(map.get(breakpoint-settings.$values, $breakpoint) == 0, "", "-#{$breakpoint}"); 11 | 12 | @each $key, $utility in utility-settings.$map { 13 | @if meta.type-of($utility) == "map" and (map.get($utility, responsive) == true or $infix == "") { 14 | @include utilities.generate($utility, $infix); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/elements/_code.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/code"; 2 | 3 | code, 4 | pre { 5 | font-size: code.$font-size; 6 | font-family: code.$font-family; 7 | } 8 | 9 | code { 10 | padding: 0.15em 0.5em; 11 | background-color: var(--rui-color-background-light); 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/elements/_links.scss: -------------------------------------------------------------------------------- 1 | @use "../tools/links"; 2 | 3 | a { 4 | @include links.base(); 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/elements/_lists.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/lists"; 2 | 3 | ol, 4 | ul { 5 | padding-left: 1.25em; 6 | } 7 | 8 | ul { 9 | list-style-type: lists.$unordered-style; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/elements/_page.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/page"; 2 | @use "../theme/typography"; 3 | 4 | html { 5 | -moz-osx-font-smoothing: grayscale; 6 | -webkit-font-smoothing: antialiased; 7 | font-weight: typography.$font-weight-base; 8 | font-size: typography.$font-size-base; 9 | line-height: typography.$line-height-base; 10 | font-family: typography.$font-family-base; 11 | } 12 | 13 | body { 14 | font-size: 1em; 15 | color: page.$color; 16 | background-color: page.$background; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/elements/_rulers.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/borders"; 2 | 3 | hr { 4 | border: 0; 5 | border-top: borders.$width solid var(--rui-color-border-secondary); 6 | background: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/elements/_small.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/typography"; 2 | 3 | small { 4 | font-size: typography.$font-size-small; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/generic/_box-sizing.scss: -------------------------------------------------------------------------------- 1 | // More sensible default box-sizing: 2 | // https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ 3 | 4 | html { 5 | box-sizing: border-box; 6 | } 7 | 8 | // stylelint-disable selector-max-universal 9 | *, 10 | *::before, 11 | *::after { 12 | box-sizing: inherit; 13 | } 14 | // stylelint-enable selector-max-universal 15 | -------------------------------------------------------------------------------- /src/styles/generic/_focus.scss: -------------------------------------------------------------------------------- 1 | @use "../tools/accessibility"; 2 | 3 | // Remove focus outline as we implement custom appearance of focus state. Increase specificity where necessary to 4 | // override normalize.css. 5 | :where(button, input, select, textarea):focus { 6 | outline: none; 7 | } 8 | 9 | :is(a, button, input, select, textarea, [type="button"], [type="submit"]):focus-visible { 10 | @include accessibility.focus-ring(); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/generic/_forms.scss: -------------------------------------------------------------------------------- 1 | // Reset Chrome and Firefox behaviour which sets a `min-width: min-content;` on fieldsets. 2 | fieldset { 3 | min-width: 0; 4 | border: 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/generic/_reset.scss: -------------------------------------------------------------------------------- 1 | body, 2 | h1, 3 | h2, 4 | h3, 5 | h4, 6 | h5, 7 | h6, 8 | blockquote, 9 | p, 10 | pre, 11 | dl, 12 | dd, 13 | ol, 14 | ul, 15 | figure, 16 | hr, 17 | fieldset, 18 | legend { 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1, 24 | h2, 25 | h3, 26 | h4, 27 | h5, 28 | h6 { 29 | font-size: 1rem; 30 | } 31 | 32 | li > ol, 33 | li > ul { 34 | margin-bottom: 0; 35 | } 36 | 37 | table { 38 | border-collapse: collapse; 39 | border-spacing: 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/generic/_shared.scss: -------------------------------------------------------------------------------- 1 | @use "../tools/spacing"; 2 | 3 | // Always declare margins in the same direction. 4 | address, 5 | blockquote, 6 | p, 7 | pre, 8 | dl, 9 | ol, 10 | ul, 11 | figure, 12 | hr, 13 | table, 14 | fieldset { 15 | @include spacing.bottom(); 16 | } 17 | 18 | h1, 19 | h2, 20 | h3, 21 | h4, 22 | h5, 23 | h6 { 24 | @include spacing.bottom(headings); 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/helpers/_animation.scss: -------------------------------------------------------------------------------- 1 | $_spin-count: 4; 2 | $_spin-duration: 2.2s; 3 | $_spin-easing: cubic-bezier(0.31, 0.3, 0.34, -0.17); 4 | 5 | @keyframes spin { 6 | 0% { 7 | transform: rotate(0 * 360deg); 8 | } 9 | 10 | 100% { 11 | transform: rotate($_spin-count * 360deg); 12 | } 13 | } 14 | 15 | .animation-spin-clockwise { 16 | animation: spin $_spin-duration $_spin-easing infinite; 17 | } 18 | 19 | .animation-spin-counterclockwise { 20 | animation: spin $_spin-duration $_spin-easing infinite reverse; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/settings/_animations.scss: -------------------------------------------------------------------------------- 1 | $standard-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 2 | -------------------------------------------------------------------------------- /src/styles/settings/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $values: ( 2 | xs: 0, 3 | sm: 36em, 4 | md: 48em, 5 | lg: 66em, 6 | xl: 84em, 7 | x2l: 100em, 8 | x3l: 120em, 9 | ); 10 | -------------------------------------------------------------------------------- /src/styles/settings/_collections.scss: -------------------------------------------------------------------------------- 1 | $action-colors: primary, secondary, selected; 2 | $feedback-colors: success, warning, danger, info, help, note; 3 | $neutral-colors: light, dark; 4 | 5 | $colors: ( 6 | action: $action-colors, 7 | feedback: $feedback-colors, 8 | neutral: $neutral-colors, 9 | ); 10 | -------------------------------------------------------------------------------- /src/styles/settings/_escaped-characters.scss: -------------------------------------------------------------------------------- 1 | $map: ( 2 | ("<", "%3c"), 3 | (">", "%3e"), 4 | ("#", "%23"), 5 | ("(", "%28"), 6 | (")", "%29"), 7 | ); 8 | -------------------------------------------------------------------------------- /src/styles/settings/_form-fields.scss: -------------------------------------------------------------------------------- 1 | // 1. Input `line-height` is specified in `rem` so inputs do not break when their font size gets 2 | // changed later. 3 | 4 | @use "sass:map"; 5 | @use "../theme/typography"; 6 | @use "../tools/spacing"; 7 | 8 | $help-text-line-height: 1.2rem; 9 | 10 | $vertical-inner-gap: spacing.of(1); 11 | $vertical-inner-gap-large: spacing.of(2); 12 | $vertical-outer-spacing: spacing.of(2); 13 | 14 | $horizontal-inner-gap: spacing.of(2); 15 | $horizontal-outer-spacing: spacing.of(2); 16 | 17 | $box-input-font-family: typography.$font-family-base; 18 | $box-input-font-weight: map.get(typography.$font-weight-values, default); 19 | $box-input-line-height: 1.5rem; // 1. 20 | $box-field-caret-size: 2.25rem; 21 | $box-field-input-number-arrows-width: 1.5rem; 22 | $box-field-bottom-line-height: 2px; 23 | 24 | $inline-field-inner-gap: spacing.of(2); 25 | 26 | $themeable-variant-states: ( 27 | box: ( 28 | filled: (default, hover, focus, disabled), 29 | outline: (default, hover, focus, disabled), 30 | ), 31 | check: ( 32 | default: (default, checked, disabled, checked-disabled), 33 | ), 34 | custom: ( 35 | default: (default, disabled), 36 | ), 37 | validation: ( 38 | invalid: (default, checked, disabled, checked-disabled), 39 | valid: (default, checked, disabled, checked-disabled), 40 | warning: (default, checked, disabled, checked-disabled), 41 | ), 42 | ); 43 | 44 | $themeable-state-properties: ( 45 | default: (color, border-color, background, check-background-color, box-shadow, surrounding-text-color), 46 | hover: (color, border-color, background, box-shadow), 47 | focus: (color, border-color, background, check-background-color, box-shadow), 48 | checked: (border-color, check-background-color), 49 | disabled: (color, border-color, background, check-background-color, box-shadow, surrounding-text-color), 50 | checked-disabled: (border-color, check-background-color), 51 | ); 52 | -------------------------------------------------------------------------------- /src/styles/settings/_forms.scss: -------------------------------------------------------------------------------- 1 | $horizontal-breakpoint: md; 2 | -------------------------------------------------------------------------------- /src/styles/theme-constants/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // ⚠️ For `theme.scss` purposes only, do not use anywhere else. 2 | 3 | @use "sass:map"; 4 | @use "../settings/breakpoints"; 5 | 6 | $xs: map.get(breakpoints.$values, xs); 7 | $sm: map.get(breakpoints.$values, sm); 8 | $md: map.get(breakpoints.$values, md); 9 | $lg: map.get(breakpoints.$values, lg); 10 | $xl: map.get(breakpoints.$values, xl); 11 | $x2l: map.get(breakpoints.$values, x2l); 12 | $x3l: map.get(breakpoints.$values, x3l); 13 | -------------------------------------------------------------------------------- /src/styles/theme-constants/_svg.scss: -------------------------------------------------------------------------------- 1 | // ⚠️ For `theme.scss` purposes only, do not use anywhere else. 2 | 3 | @use "../tools/svg"; 4 | 5 | $checkbox-checked: svg.escape(url("data:image/svg+xml,")); 6 | $radio-checked: svg.escape(url("data:image/svg+xml,")); 7 | $toggle: svg.escape(url("data:image/svg+xml,")); 8 | $toggle-checked: svg.escape(url("data:image/svg+xml,")); 9 | -------------------------------------------------------------------------------- /src/styles/theme/_accessibility.scss: -------------------------------------------------------------------------------- 1 | // 1. Make it possible to keep the original box shadow of the component: if `--rui-focus-box-shadow` is set to 2 | // `initial`, `revert` or `unset`, `--rui-local-box-shadow` is used. 3 | 4 | $tap-target-size: var(--rui-dimension-tap-target-size); 5 | $focus-outline: var(--rui-border-focus-ring); 6 | $focus-outline-offset: var(--rui-dimension-focus-ring-offset); 7 | $focus-box-shadow: var(--rui-shadow-focus-ring, var(--rui-local-box-shadow, initial)); // 1. 8 | -------------------------------------------------------------------------------- /src/styles/theme/_borders.scss: -------------------------------------------------------------------------------- 1 | $width: var(--rui-dimension-border-width-1); 2 | $radius-1: var(--rui-dimension-radius-1); 3 | $radius-2: var(--rui-dimension-radius-2); 4 | -------------------------------------------------------------------------------- /src/styles/theme/_code.scss: -------------------------------------------------------------------------------- 1 | $font-family: var(--rui-font-family-monospace); 2 | $font-size: var(--rui-font-size-code); 3 | -------------------------------------------------------------------------------- /src/styles/theme/_links.scss: -------------------------------------------------------------------------------- 1 | $color: var(--rui-local-link-color, var(--rui-color-text-link)); 2 | $decoration: var(--rui-text-decoration-link); 3 | $underline-offset: var(--rui-underline-offset-link); 4 | $hover-color: var(--rui-local-link-color-hover, var(--rui-color-text-link-hover)); 5 | $hover-decoration: var(--rui-text-decoration-link-hover); 6 | $active-color: var(--rui-local-link-color-active, var(--rui-color-text-link-active)); 7 | $active-decoration: var(--rui-text-decoration-link-active); 8 | -------------------------------------------------------------------------------- /src/styles/theme/_lists.scss: -------------------------------------------------------------------------------- 1 | $unordered-style: var(--rui-list-style-unordered); 2 | -------------------------------------------------------------------------------- /src/styles/theme/_page.scss: -------------------------------------------------------------------------------- 1 | $background: var(--rui-color-background-base); 2 | $color: var(--rui-color-text-primary); 3 | -------------------------------------------------------------------------------- /src/styles/theme/_spacing.scss: -------------------------------------------------------------------------------- 1 | $values: ( 2 | 0: var(--rui-dimension-space-0), 3 | 1: var(--rui-dimension-space-1), 4 | 2: var(--rui-dimension-space-2), 5 | 3: var(--rui-dimension-space-3), 6 | 4: var(--rui-dimension-space-4), 7 | 5: var(--rui-dimension-space-5), 8 | 6: var(--rui-dimension-space-6), 9 | 7: var(--rui-dimension-space-7), 10 | ); 11 | 12 | $bottom: ( 13 | base: var(--rui-dimension-space-bottom-base), 14 | headings: var(--rui-dimension-space-bottom-headings), 15 | layouts: var(--rui-dimension-space-bottom-layouts), 16 | ); 17 | -------------------------------------------------------------------------------- /src/styles/theme/_typography.scss: -------------------------------------------------------------------------------- 1 | $font-weight-values: ( 2 | light: var(--rui-font-weight-light), 3 | default: var(--rui-font-weight-base), 4 | bold: var(--rui-font-weight-bold), 5 | ); 6 | 7 | $font-weight-base: var(--rui-font-weight-base); 8 | 9 | $font-size-values: ( 10 | 1: var(--rui-font-size-1), 11 | 2: var(--rui-font-size-2), 12 | 3: var(--rui-font-size-3), 13 | 4: var(--rui-font-size-4), 14 | 5: var(--rui-font-size-5), 15 | 6: var(--rui-font-size-6), 16 | ); 17 | 18 | $font-size-base: var(--rui-font-size-base); 19 | $font-size-small: var(--rui-font-size-small); 20 | $font-size-smaller: var(--rui-font-size-smaller); 21 | 22 | $font-family-base: var(--rui-font-family-base); 23 | 24 | $line-height-base: var(--rui-line-height-base); 25 | $line-height-small: var(--rui-line-height-small); 26 | -------------------------------------------------------------------------------- /src/styles/tools/_accessibility.scss: -------------------------------------------------------------------------------- 1 | // 1. Screen readers only, inspired by Bootstrap. 2 | // https://github.com/twbs/bootstrap/blob/master/scss/mixins/_screen-reader.scss 3 | // 4 | // 2. Make tap target big enough to improve accessibility on touch screens. 5 | 6 | @use "../theme/accessibility" as theme; 7 | 8 | // 1. 9 | @mixin hide-text() { 10 | position: absolute; 11 | width: 1px; 12 | height: 1px; 13 | padding: 0; 14 | overflow: hidden; 15 | clip: rect(0, 0, 0, 0); 16 | white-space: nowrap; 17 | border: 0; 18 | } 19 | 20 | @mixin unhide-text() { 21 | position: unset; 22 | width: unset; 23 | height: unset; 24 | overflow: unset; 25 | clip: unset; 26 | white-space: unset; 27 | } 28 | 29 | // 2. 30 | @mixin min-tap-target($size: theme.$tap-target-size, $center: true) { 31 | position: relative; 32 | 33 | &::before { 34 | content: ""; 35 | position: absolute; 36 | width: $size; 37 | height: $size; 38 | 39 | @if $center { 40 | top: 50%; 41 | left: 50%; 42 | transform: translate(-50%, -50%); 43 | } 44 | } 45 | } 46 | 47 | @mixin focus-ring() { 48 | outline: theme.$focus-outline; 49 | outline-offset: theme.$focus-outline-offset; 50 | box-shadow: theme.$focus-box-shadow; 51 | } 52 | -------------------------------------------------------------------------------- /src/styles/tools/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../settings/breakpoints"; 3 | 4 | @mixin up($breakpoint) { 5 | @if not map.has-key(breakpoints.$values, $breakpoint) { 6 | @error "Invalid breakpoint specified! #{$breakpoint} doesn't exist. " 7 | + "Choose one of #{map.keys(breakpoints.$values)}"; 8 | } @else if map.get(breakpoints.$values, $breakpoint) == 0 { 9 | @content; 10 | } @else { 11 | @media (min-width: map.get(breakpoints.$values, $breakpoint)) { 12 | @content; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/tools/_caret.scss: -------------------------------------------------------------------------------- 1 | @mixin create($rotate: 0) { 2 | @include rotate(); 3 | 4 | display: inline-block; 5 | width: 0.4375rem; 6 | height: 0.4375rem; 7 | border-width: 2px; 8 | border-style: none solid solid none; 9 | border-color: currentcolor; 10 | } 11 | 12 | @mixin rotate($rotate: 0) { 13 | transform: translateY(-0.1rem) rotate($rotate + 45deg); 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/tools/_colors.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | @function lighten-color($color, $weight) { 4 | @return color.mix(white, $color, $weight); 5 | } 6 | 7 | @function darken-color($color, $weight) { 8 | @return color.mix(black, $color, $weight); 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/tools/_links.scss: -------------------------------------------------------------------------------- 1 | @use "../theme/links"; 2 | 3 | @mixin base() { 4 | text-decoration: links.$decoration; 5 | text-underline-offset: links.$underline-offset; 6 | color: links.$color; 7 | 8 | &:hover { 9 | text-decoration: links.$hover-decoration; 10 | color: links.$hover-color; 11 | } 12 | 13 | &:active { 14 | text-decoration: links.$active-decoration; 15 | color: links.$active-color; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/tools/_reset.scss: -------------------------------------------------------------------------------- 1 | @mixin button() { 2 | appearance: none; 3 | display: inline-flex; 4 | flex: none; 5 | align-items: center; 6 | justify-content: center; 7 | padding: 0; 8 | border: 0; 9 | border-radius: 0; 10 | background: none; 11 | box-shadow: none; 12 | cursor: pointer; 13 | } 14 | 15 | @mixin link() { 16 | text-decoration: none; 17 | color: inherit; 18 | 19 | &:hover, 20 | &:focus { 21 | text-decoration: none; 22 | } 23 | } 24 | 25 | @mixin list() { 26 | padding-left: 0; 27 | list-style: none; 28 | 29 | &:not(:last-child) { 30 | margin-bottom: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/tools/_scrollbar.scss: -------------------------------------------------------------------------------- 1 | @mixin hide() { 2 | scrollbar-width: none; 3 | 4 | &::-webkit-scrollbar { 5 | display: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/tools/_spacing.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../theme/spacing"; 3 | 4 | @function of($value) { 5 | @if $value % 1 != 0 { 6 | @error "Only whole numbers can be used for spacing, #{$value} given."; 7 | } @else { 8 | @if map.has-key(spacing.$values, $value) { 9 | @return map.get(spacing.$values, $value); 10 | } 11 | } 12 | } 13 | 14 | @mixin bottom($category: base) { 15 | @if not map.has-key(spacing.$bottom, $category) { 16 | @error "Invalid spacing category specified! #{$category} doesn't exist. " 17 | + "Choose one of #{map.keys(spacing.$bottom)}."; 18 | } 19 | 20 | &:not(:last-child) { 21 | margin-bottom: map.get(spacing.$bottom, $category); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/tools/_string.scss: -------------------------------------------------------------------------------- 1 | @use "sass:string"; 2 | 3 | @function capitalize($string) { 4 | @return string.to-upper-case(string.slice($string, 1, 1)) + string.slice($string, 2); 5 | } 6 | 7 | // Author: Hugo Giraudel 8 | @function replace($string, $search, $replace: "") { 9 | $index: string.index($string, $search); 10 | 11 | @if $index { 12 | @return string.slice($string, 1, $index - 1) 13 | + $replace 14 | + replace(string.slice($string, $index + string.length($search)), $search, $replace); 15 | } 16 | 17 | @return $string; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/tools/_svg.scss: -------------------------------------------------------------------------------- 1 | // See https://codepen.io/kevinweber/pen/dXWoRw 2 | // 3 | // 1. Do not escape the url brackets. 4 | 5 | @use "sass:string"; 6 | @use "../settings/escaped-characters"; 7 | @use "string" as rui-string; 8 | 9 | @function escape($string) { 10 | @if string.index($string, "data:image/svg+xml") { 11 | @each $char, $encoded in escaped-characters.$map { 12 | // 1. 13 | @if string.index($string, "url(") == 1 { 14 | $string: url("#{rui-string.replace(string.slice($string, 6, -3), $char, $encoded)}"); 15 | } @else { 16 | $string: rui-string.replace($string, $char, $encoded); 17 | } 18 | } 19 | } 20 | 21 | @return $string; 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/tools/_utilities.scss: -------------------------------------------------------------------------------- 1 | // Utility generator 2 | // Inspired by Bootstrap 4 3 | 4 | @use "sass:list"; 5 | @use "sass:map"; 6 | @use "sass:meta"; 7 | 8 | @mixin generate($utility, $infix) { 9 | $values: map.get($utility, values); 10 | 11 | // If the values are a list or string, convert it into a map 12 | @if meta.type-of($values) == "string" or meta.type-of(list.nth($values, 1)) != "list" { 13 | $values: list.zip($values, $values); 14 | } 15 | 16 | @each $key, $value in $values { 17 | $properties: map.get($utility, property); 18 | $property-class: map.get($utility, class); 19 | 20 | // Don't prefix if value key is null (e.g. with shadow class) 21 | $property-class-modifier: if($key, "-" + $key, ""); 22 | 23 | .#{$property-class + $infix + $property-class-modifier} { 24 | @each $property in $properties { 25 | // stylelint-disable-next-line declaration-no-important 26 | #{$property}: $value !important; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/tools/form-fields/_box-field-sizes.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "../../theme/form-fields" as theme; 3 | 4 | @mixin size($size, $is-multiline: false) { 5 | $size-properties: map.get(theme.$box-sizes, $size); 6 | 7 | --rui-local-height: #{map.get($size-properties, height)}; 8 | --rui-local-padding-y: #{map.get($size-properties, padding-y)}; 9 | --rui-local-padding-x: #{map.get($size-properties, padding-x)}; 10 | --rui-local-font-size: #{map.get($size-properties, font-size)}; 11 | 12 | @if $is-multiline { 13 | .input { 14 | height: auto; 15 | min-height: map.get($size-properties, height); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/tools/form-fields/_foundation.scss: -------------------------------------------------------------------------------- 1 | // 1. Don't let text alignment be affected by a parent. 2 | // 2. Override foundation reset. 3 | 4 | @use "../../settings/form-fields" as settings; 5 | @use "../../theme/form-fields" as theme; 6 | 7 | @mixin root() { 8 | text-align: left; // 1. 9 | } 10 | 11 | @mixin fieldset() { 12 | &:not(:last-child) { 13 | margin-bottom: 0; // 2. 14 | } 15 | } 16 | 17 | @mixin label() { 18 | color: var(--rui-local-surrounding-text-color, #{theme.$label-color}); 19 | } 20 | 21 | @mixin label-required($show-require-sign: true) { 22 | color: var(--rui-local-surrounding-text-color, #{theme.$required-label-color}); 23 | 24 | @if $show-require-sign { 25 | &::after { 26 | content: theme.$required-sign; 27 | color: theme.$required-sign-color; 28 | } 29 | } 30 | } 31 | 32 | @mixin help-text() { 33 | font-style: theme.$help-text-font-style; 34 | font-size: theme.$help-text-font-size; 35 | line-height: settings.$help-text-line-height; 36 | color: var(--rui-local-surrounding-text-color, #{theme.$help-text-color}); 37 | } 38 | -------------------------------------------------------------------------------- /src/translations/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Alert: { 3 | close: 'Close', 4 | }, 5 | FileInputField: { 6 | browse: 'Browse', 7 | drop: 'or drop file here', 8 | filesSelected: 'files selected', 9 | }, 10 | ModalCloseButton: { 11 | close: 'Close', 12 | }, 13 | ScrollView: { 14 | next: 'Next', 15 | previous: 'Previous', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/__tests__/mergeDeep.js: -------------------------------------------------------------------------------- 1 | import { mergeDeep } from '../mergeDeep'; 2 | 3 | describe('mergeDeep', () => { 4 | it('adds new attributes', () => { 5 | const obj1 = {}; 6 | const obj2 = { 7 | props: { 8 | className: 'class', 9 | style: { 10 | color: 'white', 11 | }, 12 | }, 13 | state: { 14 | items: [1, 2], 15 | itemsSize: 2, 16 | }, 17 | }; 18 | const expectedObj = { 19 | props: { 20 | className: 'class', 21 | style: { 22 | color: 'white', 23 | }, 24 | }, 25 | state: { 26 | items: [1, 2], 27 | itemsSize: 2, 28 | }, 29 | }; 30 | 31 | expect(mergeDeep(obj1, obj2)).toEqual(expectedObj); 32 | }); 33 | 34 | it('merges with existing attributes', () => { 35 | const obj1 = { 36 | props: { 37 | children: ['child1', 'child2'], 38 | className: 'class', 39 | parent: 'parent', 40 | style: { 41 | color: 'white', 42 | }, 43 | }, 44 | state: { 45 | items: [1, 2], 46 | itemsSize: 2, 47 | }, 48 | }; 49 | const obj2 = { 50 | props: { 51 | children: null, 52 | className: 'class1 class2', 53 | style: { 54 | backgroundColor: 'black', 55 | }, 56 | }, 57 | state: { 58 | items: [3, 4, 5], 59 | itemsSize: 3, 60 | }, 61 | }; 62 | const expectedObj = { 63 | props: { 64 | children: null, 65 | className: 'class1 class2', 66 | parent: 'parent', 67 | style: { 68 | backgroundColor: 'black', 69 | color: 'white', 70 | }, 71 | }, 72 | state: { 73 | items: [3, 4, 5], 74 | itemsSize: 3, 75 | }, 76 | }; 77 | 78 | expect(mergeDeep(obj1, obj2)).toEqual(expectedObj); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/utils/mergeDeep.js: -------------------------------------------------------------------------------- 1 | const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj); 2 | 3 | /** 4 | * Performs a deep merge of objects and returns new object. 5 | * 6 | * @param {...object} objects 7 | * @returns {object} 8 | */ 9 | export const mergeDeep = (...objects) => objects.reduce((prev, obj) => { 10 | if (obj == null) { 11 | return prev; 12 | } 13 | 14 | const newObject = { ...prev }; 15 | 16 | Object.keys(obj).forEach((key) => { 17 | const previousVal = prev[key]; 18 | const currentVal = obj[key]; 19 | 20 | if (isObject(previousVal) && isObject(currentVal)) { 21 | newObject[key] = mergeDeep(previousVal, currentVal); 22 | } else { 23 | newObject[key] = currentVal; 24 | } 25 | }); 26 | 27 | return newObject; 28 | }, {}); 29 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@visionappscz/stylelint-config', 4 | '@visionappscz/stylelint-config/order', 5 | '@visionappscz/stylelint-config/scss', 6 | '@visionappscz/stylelint-config/cssModules', 7 | '@stylistic/stylelint-config', 8 | ], 9 | rules: { 10 | // Allow @else and @elseif to be on the same line as the closing brace of the @if block. 11 | '@stylistic/block-closing-brace-newline-after': 'always-single-line', 12 | 13 | // Use 4 spaces for indentation. 14 | '@stylistic/indentation': 4, 15 | 16 | // Check that custom property name starts with `rui` prefix and follows either SUIT CSS convention 17 | // (for components theming) or kebab-case syntax (for global design tokens and local properties). 18 | // 19 | // A: mandatory prefix 20 | // B: SUIT CSS pattern (derived from BEM pattern: https://gist.github.com/Potherca/f2a65491e63338659c3a0d2b07eee382) 21 | // C: kebab-case pattern 22 | // D: … with optional Sass interpolation used in generated custom properties (e.g. Button or form fields) 23 | 'custom-property-pattern': [ 24 | // ↓ A ↓ B OR ↓ C ↓ D 25 | '^rui-(?:([A-Z]([A-Za-z0-9-]+)?((__([a-z0-9]+-?)+)+(--([a-z0-9]+-?)+){0,2})+)|(([a-z0-9]+-?)+)(#{\\$[a-z-]+})?)$', 26 | { 27 | message: 'Expected custom property name to start with `rui-*` and follow either SUIT CSS or kebab-case syntax', 28 | }, 29 | ], 30 | 31 | // Require camelCase pattern for class names as they are picked up by dot notation in JS. 32 | // Also allow kebab-case class names for global helper and utility classes. 33 | // 34 | // A: camelCase pattern 35 | // B: kebab-case pattern 36 | 'selector-class-pattern': [ 37 | // ↓ A OR ↓ B 38 | '^(?:(([a-z][a-z0-9]*)([A-Z][a-z0-9]+)*)|(([a-z0-9]+-?)+))$', 39 | { 40 | message: 'Expected class selector to be either camelCase (CSS Modules) or kebab-case (global classes)', 41 | }, 42 | ], 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /tests/mocks/svgrMock.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SvgrMock = (props) => (); 4 | export default SvgrMock; 5 | -------------------------------------------------------------------------------- /tests/propTests/actionColorPropTest.js: -------------------------------------------------------------------------------- 1 | export const actionColorPropTest = [ 2 | [ 3 | { color: 'primary' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootColorPrimary'), 5 | ], 6 | [ 7 | { color: 'secondary' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootColorSecondary'), 9 | ], 10 | [ 11 | { color: 'selected' }, 12 | (rootElement) => expect(rootElement).toHaveClass('isRootColorSelected'), 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/propTests/alignPropTest.js: -------------------------------------------------------------------------------- 1 | export const alignPropTest = (itemType) => ( 2 | [ 3 | [ 4 | { align: 'top' }, 5 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}AlignedToTop`), 6 | ], 7 | [ 8 | { align: 'middle' }, 9 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}AlignedToMiddle`), 10 | ], 11 | [ 12 | { align: 'bottom' }, 13 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}AlignedToBottom`), 14 | ], 15 | [ 16 | { align: 'baseline' }, 17 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}AlignedToBaseline`), 18 | ], 19 | ] 20 | ); 21 | 22 | -------------------------------------------------------------------------------- /tests/propTests/blockPropTest.js: -------------------------------------------------------------------------------- 1 | export const blockPropTest = [ 2 | [ 3 | { block: true }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootBlock'), 5 | ], 6 | [ 7 | { block: false }, 8 | (rootElement) => expect(rootElement).not.toHaveClass('isRootBlock'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/childrenEmptyPropTest.js: -------------------------------------------------------------------------------- 1 | export const childrenEmptyPropTest = [ 2 | [ 3 | { children: undefined }, 4 | (rootElement) => expect(rootElement).toBeNull(), 5 | ], 6 | [ 7 | { children: null }, 8 | (rootElement) => expect(rootElement).toBeNull(), 9 | ], 10 | [ 11 | { children: false }, 12 | (rootElement) => expect(rootElement).toBeNull(), 13 | ], 14 | [ 15 | { children: [] }, 16 | (rootElement) => expect(rootElement).toBeNull(), 17 | ], 18 | ]; 19 | 20 | -------------------------------------------------------------------------------- /tests/propTests/densePropTest.js: -------------------------------------------------------------------------------- 1 | export const densePropTest = (itemType) => ( 2 | [ 3 | [ 4 | { dense: true }, 5 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}Dense`), 6 | ], 7 | [ 8 | { dense: false }, 9 | (rootElement) => expect(rootElement).not.toHaveClass(`is${itemType}Dense`), 10 | ], 11 | ] 12 | ); 13 | 14 | -------------------------------------------------------------------------------- /tests/propTests/disabledPropTest.js: -------------------------------------------------------------------------------- 1 | import { within } from '@testing-library/react'; 2 | 3 | export const disabledPropTest = [ 4 | [ 5 | { disabled: true }, 6 | (rootElement) => { 7 | expect(rootElement).toHaveClass('isRootDisabled'); 8 | expect(within(rootElement).getByLabelText('label')).toBeDisabled(); 9 | }, 10 | ], 11 | [ 12 | { disabled: false }, 13 | (rootElement) => { 14 | expect(rootElement).not.toHaveClass('isRootDisabled'); 15 | expect(within(rootElement).getByLabelText('label')).not.toBeDisabled(); 16 | }, 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /tests/propTests/feedbackColorPropTest.js: -------------------------------------------------------------------------------- 1 | export const feedbackColorPropTest = [ 2 | [ 3 | { color: 'danger' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootColorDanger'), 5 | ], 6 | [ 7 | { color: 'help' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootColorHelp'), 9 | ], 10 | [ 11 | { color: 'info' }, 12 | (rootElement) => expect(rootElement).toHaveClass('isRootColorInfo'), 13 | ], 14 | [ 15 | { color: 'note' }, 16 | (rootElement) => expect(rootElement).toHaveClass('isRootColorNote'), 17 | ], 18 | [ 19 | { color: 'success' }, 20 | (rootElement) => expect(rootElement).toHaveClass('isRootColorSuccess'), 21 | ], 22 | [ 23 | { color: 'warning' }, 24 | (rootElement) => expect(rootElement).toHaveClass('isRootColorWarning'), 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /tests/propTests/fullWidthPropTest.js: -------------------------------------------------------------------------------- 1 | export const fullWidthPropTest = [ 2 | [ 3 | { fullWidth: true }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootFullWidth'), 5 | ], 6 | [ 7 | { fullWidth: false }, 8 | (rootElement) => expect(rootElement).not.toHaveClass('isRootFullWidth'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/helpTextPropTest.jsx: -------------------------------------------------------------------------------- 1 | import { within } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | export const helpTextPropTest = [ 5 | [ 6 | { helpText:
help text
}, 7 | (rootElement) => expect(within(rootElement).getByText('help text')).toBeInTheDocument(), 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /tests/propTests/idPropTest.js: -------------------------------------------------------------------------------- 1 | export const idPropTest = [ 2 | [ 3 | { id: 'id' }, 4 | (rootElement) => expect(rootElement).toHaveAttribute('id', 'id'), 5 | ], 6 | ]; 7 | -------------------------------------------------------------------------------- /tests/propTests/isLabelVisibleTest.js: -------------------------------------------------------------------------------- 1 | import { within } from '@testing-library/react'; 2 | 3 | export const isLabelVisibleTest = (HtmlTag = 'label') => ( 4 | [ 5 | [ 6 | { 7 | id: 'id', 8 | isLabelVisible: true, 9 | }, 10 | (rootElement) => { 11 | if (HtmlTag === 'legend') { 12 | expect(within(rootElement).getByTestId('id__displayLabel')).not.toHaveClass('isLabelHidden'); 13 | } else { 14 | expect(within(rootElement).getByText('label')).not.toHaveClass('isLabelHidden'); 15 | } 16 | }, 17 | ], 18 | [ 19 | { 20 | id: 'id', 21 | isLabelVisible: false, 22 | }, 23 | (rootElement) => { 24 | if (HtmlTag === 'legend') { 25 | expect(within(rootElement).queryByTestId('id__displayLabel')).not.toBeInTheDocument(); 26 | } else { 27 | expect(within(rootElement).getByText('label')).toHaveClass('isLabelHidden'); 28 | } 29 | }, 30 | ], 31 | ] 32 | ); 33 | -------------------------------------------------------------------------------- /tests/propTests/justifyPropTest.js: -------------------------------------------------------------------------------- 1 | export const justifyPropTest = (itemType) => { 2 | const tests = [ 3 | [ 4 | { justify: 'start' }, 5 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}JustifiedToStart`), 6 | ], 7 | [ 8 | { justify: 'center' }, 9 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}JustifiedToCenter`), 10 | ], 11 | [ 12 | { justify: 'end' }, 13 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}JustifiedToEnd`), 14 | ], 15 | [ 16 | { justify: 'space-between' }, 17 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}JustifiedToSpaceBetween`), 18 | ], 19 | ]; 20 | if (itemType === 'Root') { 21 | tests.push([ 22 | { justify: 'stretch' }, 23 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}JustifiedToStretch`), 24 | ]); 25 | } 26 | 27 | return tests; 28 | }; 29 | -------------------------------------------------------------------------------- /tests/propTests/labelPropTest.js: -------------------------------------------------------------------------------- 1 | import { within } from '@testing-library/react'; 2 | 3 | export const labelPropTest = (type = 'label') => ( 4 | [ 5 | [ 6 | { 7 | id: 'id', 8 | label: 'label text', 9 | }, 10 | (rootElement) => { 11 | if (type === 'legend') { 12 | expect(within(rootElement).getByTestId('id__displayLabel')).toHaveTextContent('label text'); 13 | expect(within(rootElement).getByTestId('id__label')).toHaveTextContent('label text'); 14 | } else { 15 | expect(within(rootElement).getByText('label text')).toBeInTheDocument(); 16 | } 17 | }, 18 | ], 19 | ] 20 | ); 21 | -------------------------------------------------------------------------------- /tests/propTests/layoutPropTest.js: -------------------------------------------------------------------------------- 1 | export const layoutPropTest = [ 2 | [ 3 | { layout: 'vertical' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootLayoutVertical'), 5 | ], 6 | [ 7 | { layout: 'horizontal' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootLayoutHorizontal'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/neutralColorPropTest.js: -------------------------------------------------------------------------------- 1 | export const neutralColorPropTest = [ 2 | [ 3 | { color: 'dark' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootColorDark'), 5 | ], 6 | [ 7 | { color: 'light' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootColorLight'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/noWrapPropTest.js: -------------------------------------------------------------------------------- 1 | export const noWrapPropTest = (itemType) => ( 2 | [ 3 | [ 4 | { nowrap: true }, 5 | (rootElement) => expect(rootElement).toHaveClass(`is${itemType}Nowrap`), 6 | ], 7 | [ 8 | { nowrap: false }, 9 | (rootElement) => expect(rootElement).not.toHaveClass(`is${itemType}Nowrap`), 10 | ], 11 | ] 12 | ); 13 | -------------------------------------------------------------------------------- /tests/propTests/raisedPropTest.js: -------------------------------------------------------------------------------- 1 | export const raisedPropTest = [ 2 | [ 3 | { raised: true }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootRaised'), 5 | ], 6 | [ 7 | { raised: false }, 8 | (rootElement) => expect(rootElement).not.toHaveClass('isRootRaised'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/refPropTest.js: -------------------------------------------------------------------------------- 1 | export const refPropTest = (ref) => [ 2 | [ 3 | { 4 | id: 'id', 5 | ref, 6 | }, 7 | () => expect(ref.current).toHaveAttribute('id', 'id'), 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /tests/propTests/renderAsRequiredPropTest.js: -------------------------------------------------------------------------------- 1 | export const renderAsRequiredPropTest = [ 2 | [ 3 | { renderAsRequired: true }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootRequired'), 5 | ], 6 | [ 7 | { renderAsRequired: false }, 8 | (rootElement) => expect(rootElement).not.toHaveClass('isRootRequired'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/requiredPropTest.js: -------------------------------------------------------------------------------- 1 | export const requiredPropTest = [ 2 | [ 3 | { required: true }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootRequired'), 5 | ], 6 | [ 7 | { required: false }, 8 | (rootElement) => expect(rootElement).not.toHaveClass('isRootRequired'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/propTests/sizePropTest.js: -------------------------------------------------------------------------------- 1 | export const sizePropTest = [ 2 | [ 3 | { size: 'small' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootSizeSmall'), 5 | ], 6 | [ 7 | { size: 'medium' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootSizeMedium'), 9 | ], 10 | [ 11 | { size: 'large' }, 12 | (rootElement) => expect(rootElement).toHaveClass('isRootSizeLarge'), 13 | ], 14 | 15 | ]; 16 | -------------------------------------------------------------------------------- /tests/propTests/tagPropTest.js: -------------------------------------------------------------------------------- 1 | export const tagPropTest = [ 2 | [ 3 | { tag: 'section' }, 4 | (rootElement) => expect(rootElement.tagName).toEqual('SECTION'), 5 | ], 6 | ]; 7 | -------------------------------------------------------------------------------- /tests/propTests/validationStatePropTest.js: -------------------------------------------------------------------------------- 1 | export const validationStatePropTest = [ 2 | [ 3 | { validationState: 'invalid' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootStateInvalid'), 5 | ], 6 | [ 7 | { validationState: 'valid' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootStateValid'), 9 | ], 10 | [ 11 | { validationState: 'warning' }, 12 | (rootElement) => expect(rootElement).toHaveClass('isRootStateWarning'), 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/propTests/validationTextPropTest.jsx: -------------------------------------------------------------------------------- 1 | import { within } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | export const validationTextPropTest = [ 5 | [ 6 | { validationText:
validation text
}, 7 | (rootElement) => expect(within(rootElement).getByText('validation text')).toBeInTheDocument(), 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /tests/propTests/variantPropTest.js: -------------------------------------------------------------------------------- 1 | export const variantPropTest = [ 2 | [ 3 | { variant: 'filled' }, 4 | (rootElement) => expect(rootElement).toHaveClass('isRootVariantFilled'), 5 | ], 6 | [ 7 | { variant: 'outline' }, 8 | (rootElement) => expect(rootElement).toHaveClass('isRootVariantOutline'), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/providerTests/formLayoutProviderTest.jsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | import { FormLayoutContext } from '../../src/components/FormLayout'; 4 | 5 | export const formLayoutProviderTest = (Component) => { 6 | it.each([ 7 | [ 8 | { layout: 'vertical' }, 9 | (rootElement) => { 10 | expect(rootElement).toHaveClass('isRootInFormLayout'); 11 | expect(rootElement).toHaveClass('isRootLayoutVertical'); 12 | }, 13 | ], 14 | [ 15 | { layout: 'horizontal' }, 16 | (rootElement) => { 17 | expect(rootElement).toHaveClass('isRootInFormLayout'); 18 | expect(rootElement).toHaveClass('isRootLayoutHorizontal'); 19 | }, 20 | ], 21 | ])('renders with FormLayout props: "%s"', (testedProps, assert) => { 22 | const dom = render(( 23 | 26 | {Component} 27 | 28 | )); 29 | 30 | assert(dom.container.firstChild); 31 | }); 32 | 33 | it('renders without FormLayout', () => { 34 | const dom = render(( 35 | Component 36 | )); 37 | 38 | expect(dom.container.firstChild).not.toHaveClass('isRootInFormLayout'); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /tests/setupJest.js: -------------------------------------------------------------------------------- 1 | console.warning = (error) => { 2 | throw new Error(error); 3 | }; 4 | 5 | console.error = (error) => { 6 | throw new Error(error); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/setupTestingLibrary.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { configure } from '@testing-library/react' 3 | 4 | configure({ testIdAttribute: 'id' }) 5 | --------------------------------------------------------------------------------