diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b8db85ee4..c8ba044f0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1575,6 +1575,25 @@ importers: specifier: workspace:* version: link:../../tools/testing-utils + semcore/radio-cards: + dependencies: + '@semcore/dot': + specifier: ^17.2.1 + version: link:../dot + '@semcore/typography': + specifier: ^17.2.1 + version: link:../typography + devDependencies: + '@semcore/base-components': + specifier: workspace:* + version: link:../base-components + '@semcore/core': + specifier: workspace:* + version: link:../core + '@semcore/testing-utils': + specifier: workspace:* + version: link:../../tools/testing-utils + semcore/select: dependencies: '@semcore/base-trigger': @@ -19566,7 +19585,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.48.0)(yaml@2.8.3) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.48.0)(yaml@2.8.3) '@vitest/utils@3.2.4': dependencies: diff --git a/semcore/pills/__tests__/pills.axe-test.ts b/semcore/pills/__tests__/pills.axe-test.ts index 687ce55f7c..ed2409fd48 100644 --- a/semcore/pills/__tests__/pills.axe-test.ts +++ b/semcore/pills/__tests__/pills.axe-test.ts @@ -16,11 +16,4 @@ test.describe(`@pills ${TAG.ACCESSIBILITY}`, () => { const violations = await getAccessibilityViolations({ page }); expect(violations).toEqual([]); }); - - test('Custom pills example', async ({ page }) => { - await loadPage(page, 'stories/components/pills/docs/examples/custom_pills_example.tsx', 'en'); - - const violations = await getAccessibilityViolations({ page }); - expect(violations).toEqual([]); - }); }); diff --git a/semcore/pills/__tests__/pills.browser-test.tsx b/semcore/pills/__tests__/pills.browser-test.tsx index b1bf88e3a6..dadc1edec1 100644 --- a/semcore/pills/__tests__/pills.browser-test.tsx +++ b/semcore/pills/__tests__/pills.browser-test.tsx @@ -68,28 +68,6 @@ test.describe(`${TAG.VISUAL}`, () => { }); }); - test('Verify custom pills', { - tag: [TAG.PRIORITY_HIGH, '@pills'], - }, async ({ page }) => { - await loadPage(page, 'stories/components/pills/docs/examples/custom_pills_example.tsx', 'en'); - - await test.step('Verify hover state', async () => { - await locators.pillsItem(page).nth(1).hover(); - await expect(page).toHaveScreenshot(); - }); - - await test.step('Verify focus state', async () => { - await page.keyboard.press('Tab'); - await expect(page).toHaveScreenshot(); - }); - - await test.step('Verify navigation and hover', async () => { - await page.keyboard.press('ArrowRight'); - await locators.pillsItem(page).nth(1).hover(); - await expect(page).toHaveScreenshot(); - }); - }); - test('Verify pills with tab panel', { tag: [TAG.PRIORITY_HIGH, '@pills'], }, async ({ page }) => { diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png deleted file mode 100644 index 5b906eca15..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png deleted file mode 100644 index ddae558e33..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png deleted file mode 100644 index edb3f07702..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png deleted file mode 100644 index da235bd7f1..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png deleted file mode 100644 index 3eb4e8d993..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png deleted file mode 100644 index e3400fc345..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png deleted file mode 100644 index 5051d46c32..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png deleted file mode 100644 index c1767778a3..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png and /dev/null differ diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png deleted file mode 100644 index f3186577ef..0000000000 Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png and /dev/null differ diff --git a/semcore/radio-cards/.npmignore b/semcore/radio-cards/.npmignore new file mode 100644 index 0000000000..036da2936b --- /dev/null +++ b/semcore/radio-cards/.npmignore @@ -0,0 +1,5 @@ +node_modules +__tests__ +src +tsconfig.json +tsconfig.tsbuildinfo diff --git a/semcore/radio-cards/CHANGELOG.md b/semcore/radio-cards/CHANGELOG.md new file mode 100644 index 0000000000..edc662bcc8 --- /dev/null +++ b/semcore/radio-cards/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +CHANGELOG.md standards are inspired by [keepachangelog.com](https://keepachangelog.com/en/1.0.0/). + +## [18.0.0] - 2026-08-04 + +### Added + +- `RadioCards` component. diff --git a/semcore/radio-cards/README.md b/semcore/radio-cards/README.md new file mode 100644 index 0000000000..08de0012d7 --- /dev/null +++ b/semcore/radio-cards/README.md @@ -0,0 +1,37 @@ +# @semcore/radio-cards + +[![version](https://img.shields.io/npm/v/@semcore/radio-cards.svg)](https://www.npmjs.com/@semcore/radio-cards) +[![downloads](https://img.shields.io/npm/dt/@semcore/radio-cards.svg)](https://www.npmjs.com/package/@semcore/radio-cards) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/semrush/intergalactic/blob/HEAD/LICENSE) + +> This component is part of the Intergalactic Design System + +### πŸ“– [Component documentation](https://developer.semrush.com/intergalactic/components/radio-cards/) + +### 🏠 [Design system](https://developer.semrush.com/intergalactic/) + +## Install + +```sh +npm install @semcore/radio-cards +``` + + + +## πŸ‘€ Author + +[UI-kit team](https://github.com/semrush/intergalactic/blob/HEAD/MAINTAINERS) and [others ❀️](https://github.com/semrush/intergalactic/graphs/contributors) + +## 🀝 Contributing + +Contributions, issues and feature requests are welcome! + +Feel free to check [issues page](https://github.com/semrush/intergalactic/issues). You can also take a look at the [contributing guide](https://github.com/semrush/intergalactic/blob/HEAD/CONTRIBUTING.md). + +## Show your support + +Give a ⭐️ if this project helped you! + +## πŸ“ License + +This project is [MIT](https://github.com/semrush/intergalactic/blob/HEAD/LICENSE) licensed. diff --git a/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap b/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..57eb0b84dc --- /dev/null +++ b/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,42 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RadioCards > Verify data-ui-name 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [], + "uiName": "Text", + }, + ], + "uiName": "Flex", + }, + ], + "uiName": "RadioCards.Item", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "uiName": "Text", + }, + ], + "uiName": "Flex", + }, + ], + "uiName": "RadioCards.Item", + }, + ], + "uiName": "RadioCards", + }, + ], + "uiName": null, +} +`; diff --git a/semcore/radio-cards/__tests__/index.test.tsx b/semcore/radio-cards/__tests__/index.test.tsx new file mode 100644 index 0000000000..58c4d2ee65 --- /dev/null +++ b/semcore/radio-cards/__tests__/index.test.tsx @@ -0,0 +1,107 @@ +import { extractUIName } from '@semcore/testing-utils/shared/extractUINameTree.ts'; +import { runDependencyCheckTests } from '@semcore/testing-utils/shared-tests'; +import { render, cleanup, userEvent } from '@semcore/testing-utils/testing-library'; +import { expect, test, describe, beforeEach, vi } from '@semcore/testing-utils/vitest'; +import React from 'react'; + +import RadioCards from '../src'; + +describe('pills Dependency imports', () => { + runDependencyCheckTests('radio-cards'); +}); + +describe('RadioCards', () => { + beforeEach(cleanup); + + test('Verify data-ui-name', () => { + const radioCards = ( + + + + + ); + + const { container } = render(radioCards); + const result = extractUIName(container); + + expect(result).toMatchSnapshot(); + }); + + test('calls onChange with selected value', async () => { + const onChange = vi.fn(); + + const { getByRole } = render( + + 1 + 2 + 4 + , + ); + + await userEvent.click(getByRole('radio', { name: '4' })); + + expect(onChange).toHaveBeenCalledWith( + '4', + expect.any(Object), + ); + }); + + test('clicks aren\'t propagated on disabled tab', async () => { + const spy = vi.fn(); + + const { getByRole } = render( + + 1 + 2 + 3 + 4 + , + ); + + await expect(userEvent.click(getByRole('radio', { name: '4' }))).rejects.toThrow('pointer-events: none'); + expect(spy).not.toHaveBeenCalled(); + }); + + test('does not call onChange when group is disabled', async () => { + const spy = vi.fn(); + + const { getByRole } = render( + + 1 + 2 + , + ); + + await expect(userEvent.click(getByRole('radio', { name: '2' }))).rejects.toThrow('pointer-events: none'); + expect(spy).not.toHaveBeenCalled(); + }); + + test('supports keyboard navigation with ArrowRight', async () => { + const spy = vi.fn(); + + const { getByRole } = render( + + 1 + 2 + 3 + , + ); + + const firstRadio = getByRole('radio', { name: '1' }); + + firstRadio.focus(); + expect(firstRadio).toHaveFocus(); + + await userEvent.keyboard('{ArrowRight}'); + + expect(spy).toHaveBeenCalledWith( + '2', + expect.any(Object), + ); + }); +}); diff --git a/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx b/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx new file mode 100644 index 0000000000..e101474a6f --- /dev/null +++ b/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx @@ -0,0 +1,39 @@ +import { expect, test, getAccessibilityViolations } from '@semcore/testing-utils/playwright'; +import { loadPage } from '@semcore/testing-utils/shared/helpers'; +import { TAG } from '@semcore/testing-utils/shared/tags'; + +test.describe(`@radio-cards ${TAG.ACCESSIBILITY}`, () => { + test('Basic', async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/docs/examples/basic.tsx', 'en'); + + { + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + } + + { + await page.keyboard.press('Tab'); + await page.keyboard.press('ArrowRight'); + + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + } + }); + + test('Advanced', async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/docs/examples/advanced.tsx', 'en'); + + { + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + } + + { + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + } + }); +}); diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx new file mode 100644 index 0000000000..da58703eac --- /dev/null +++ b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx @@ -0,0 +1,104 @@ +import type { Page } from '@semcore/testing-utils/playwright'; +import { test, expect } from '@semcore/testing-utils/playwright'; +import { loadPage } from '@semcore/testing-utils/shared/helpers'; +import { TAG } from '@semcore/testing-utils/shared/tags'; + +export const locators = { + group: (page: Page) => page.getByRole('radiogroup'), + card: (page: Page, index?: number) => { + const base = page.getByRole('radio'); + return typeof index === 'number' ? base.nth(index) : base; + }, +}; + +/* ===================================================== +@visual +Visual states, hover and focus styles, paddings, margins, and snapshots. +===================================================== */ +test.describe(`${TAG.VISUAL}`, () => { + test('Verify default, hover, focus and disabled states', { + tag: [TAG.PRIORITY_HIGH, '@radio-cards'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en'); + + await test.step('Verify initial render with a selected card', async () => { + await expect(page).toHaveScreenshot('initial-render-with-selected-card.png'); + }); + + await test.step('Verify hover state', async () => { + await locators.card(page, 0).hover(); + await expect(page).toHaveScreenshot('hover-state.png'); + }); + + await test.step('Verify focus state', async () => { + await page.keyboard.press('Tab'); + await expect(page).toHaveScreenshot('focus-state.png'); + }); + + await test.step('Verify disabled card opacity', async () => { + await expect(locators.card(page, 2)).toHaveCSS('opacity', '0.4'); + }); + }); + + test('Verify selected state after click', { + tag: [TAG.PRIORITY_HIGH, '@radio-cards'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en'); + + await locators.card(page, 3).click(); + await expect(page).toHaveScreenshot('selected-state-after-click.png'); + }); +}); + +/* ===================================================== +@functional +Keyboard and mouse interactions - no snapshots here. +We verify states, visibility, and attributes. +===================================================== */ +test.describe(`${TAG.FUNCTIONAL}`, () => { + test('Verify click selects a card', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@radio-cards'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en'); + + await test.step('Click on first card selects it', async () => { + await locators.card(page, 0).click(); + + await expect(locators.card(page, 0)).toHaveAttribute('aria-checked', 'true'); + await expect(locators.card(page, 1)).toHaveAttribute('aria-checked', 'false'); + }); + }); + + test('Verify disabled card cannot be selected', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@radio-cards'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en'); + + await expect(locators.card(page, 2)).toHaveAttribute('aria-disabled', 'true'); + await expect(locators.card(page, 2)).toHaveCSS('pointer-events', 'none'); + }); + + test('Verify roving tabindex', { + tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@radio-cards'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en'); + + await test.step('Only the selected card is tabbable', async () => { + await expect(locators.card(page, 0)).toHaveAttribute('tabindex', '-1'); + await expect(locators.card(page, 1)).toHaveAttribute('tabindex', '0'); + await expect(locators.card(page, 3)).toHaveAttribute('tabindex', '-1'); + }); + + await test.step('ArrowRight skips the disabled card and moves to the next enabled one', async () => { + await page.keyboard.press('Tab'); + await expect(locators.card(page, 1)).toBeFocused(); + + await page.keyboard.press('ArrowRight'); + + await expect(locators.card(page, 3)).toBeFocused(); + await expect(locators.card(page, 3)).toHaveAttribute('aria-checked', 'true'); + }); + }); + + // TODO: cover ArrowLeft wrap-around, group `disabled` prop, and uncontrolled usage without an initial `value` +}); diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png new file mode 100644 index 0000000000..d5b529bf55 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png new file mode 100644 index 0000000000..85eedc16cf Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png new file mode 100644 index 0000000000..01f2a1aaa0 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png new file mode 100644 index 0000000000..27849be52e Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png new file mode 100644 index 0000000000..6283c0aa7d Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png new file mode 100644 index 0000000000..c30a8648b0 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png new file mode 100644 index 0000000000..52fb30fe5b Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png new file mode 100644 index 0000000000..6283c0aa7d Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png new file mode 100644 index 0000000000..f03acf6542 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png new file mode 100644 index 0000000000..29b3984f53 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png new file mode 100644 index 0000000000..1289f5d539 Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png differ diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png new file mode 100644 index 0000000000..140628aa6b Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png differ diff --git a/semcore/radio-cards/package.json b/semcore/radio-cards/package.json new file mode 100644 index 0000000000..33e954d761 --- /dev/null +++ b/semcore/radio-cards/package.json @@ -0,0 +1,36 @@ +{ + "name": "@semcore/radio-cards", + "description": "Semrush RadioCards Component", + "version": "18.0.0", + "main": "lib/cjs/index.js", + "module": "lib/es6/index.js", + "typings": "lib/types/index.d.ts", + "sideEffects": false, + "author": "UI-kit team ", + "license": "MIT", + "scripts": { + "build": "pnpm semcore-builder && pnpm vite build" + }, + "exports": { + "types": "./lib/types/index.d.ts", + "import": "./lib/esm/index.mjs", + "require": "./lib/cjs/index.js" + }, + "dependencies": { + "@semcore/typography": "^17.2.1", + "@semcore/dot": "^17.2.1" + }, + "peerDependencies": { + "@semcore/base-components": "^17.2.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/semrush/intergalactic.git", + "directory": "semcore/radio-cards" + }, + "devDependencies": { + "@semcore/core": "workspace:*", + "@semcore/base-components": "workspace:*", + "@semcore/testing-utils": "workspace:*" + } +} diff --git a/semcore/radio-cards/src/RadioCards.tsx b/semcore/radio-cards/src/RadioCards.tsx new file mode 100644 index 0000000000..7a642433e1 --- /dev/null +++ b/semcore/radio-cards/src/RadioCards.tsx @@ -0,0 +1,165 @@ +import { Flex, NeighborLocation } from '@semcore/base-components'; +import type { Intergalactic } from '@semcore/core'; +import { Root, Component, createComponent, sstyled } from '@semcore/core'; +import a11yEnhance from '@semcore/core/lib/utils/enhances/a11yEnhance'; +import Dot from '@semcore/dot'; +import { Text as SemcoreText } from '@semcore/typography'; +import React from 'react'; + +import type { NSRadioCards } from './RadioCards.type'; +import style from './styles/radio-cards.shadow.css'; + +const DEFAULT_VALUE = ''; + +class RadioCardsRoot extends Component< + Intergalactic.InternalTypings.InferComponentProps, + typeof RadioCardsRoot.enhance, + NSRadioCards.Handlers, + {}, + {}, + NSRadioCards.DefaultProps +> { + static displayName = 'RadioCards'; + static style = style; + + static enhance = [a11yEnhance({ + onNeighborChange: (neighborElement) => { + if (neighborElement) { + neighborElement.focus(); + neighborElement.click(); + } + }, + childSelector: () => { + const selector: [string, string] = ['role', 'radio']; + + return selector; + }, + })] as const; + + static defaultProps = { + defaultValue: DEFAULT_VALUE, + } as const; + + private firstNonDisabledItemIdx: number | null = null; + + componentDidUpdate() { + this.firstNonDisabledItemIdx = null; + } + + uncontrolledProps() { + return { + value: DEFAULT_VALUE, + }; + } + + handleClick = (value: NSRadioCards.Value) => (e: React.MouseEvent) => { + this.handlers.value(value, e); + }; + + handleKeyDown = (value: NSRadioCards.Value) => (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + + this.handlers.value(value, e); + } + }; + + getTabIndex({ props, index }: { props: NSRadioCards.Item.Props; index: number }) { + const { value, disabled: rootDisabled } = this.asProps; + const { value: itemValue, disabled: itemDisabled } = props; + const isChecked = value === itemValue; + + if (rootDisabled) return -1; + + if (this.firstNonDisabledItemIdx === null && !itemDisabled) { + this.firstNonDisabledItemIdx = index; + } + + if (index === this.firstNonDisabledItemIdx && value === DEFAULT_VALUE) { + return 0; + } + + return isChecked ? 0 : -1; + } + + getItemProps(props: NSRadioCards.Item.Props, index: number) { + const { value: itemValue } = props; + const { value, disabled } = this.asProps; + const isChecked = value === itemValue; + + return { + tabIndex: this.getTabIndex({ props, index }), + checked: isChecked, + disabled, + onClick: this.handleClick(itemValue), + onKeyDown: this.handleKeyDown(itemValue), + }; + } + + render() { + const SRadioCards = Root; + const { Children, styles, disabled } = this.asProps; + + return sstyled(styles)( + + + + + , + ); + } +} + +function Item( + props: Intergalactic.InternalTypings.InferChildComponentProps< + NSRadioCards.Item.Component, + typeof RadioCardsRoot, + 'Item' + >, +) { + const SRadioItem = Root; + const SRadioItemHeader = Flex; + const SRadioItemHeaderLeftAddon = Flex; + const SRadioItemHeaderText = SemcoreText; + const SRadioItemHeaderRightAddon = SemcoreText; + const SRadioItemDescription = SemcoreText; + const { Children, styles, iconAddon, text, textAddon, description, disabled, checked, dot, children } = props; + + const isAdvancedMode = children !== undefined; + + return sstyled(styles)( + + {isAdvancedMode + ? ( + + ) + : ( + <> + + {iconAddon && {iconAddon}} + {text && {text}} + {textAddon && ( + + {textAddon} + + )} + + {description && {description}} + {dot && } + + )} + , + ); +} + +const RadioCards = createComponent(RadioCardsRoot, { + Item, +}); + +export default RadioCards; diff --git a/semcore/radio-cards/src/RadioCards.type.ts b/semcore/radio-cards/src/RadioCards.type.ts new file mode 100644 index 0000000000..57da62a5ad --- /dev/null +++ b/semcore/radio-cards/src/RadioCards.type.ts @@ -0,0 +1,52 @@ +import type { NSFlex } from '@semcore/base-components'; +import type { Intergalactic } from '@semcore/core'; + +declare namespace NSRadioCards { + type Value = string; + type Props = NSFlex.Props & Intergalactic.RequireAtLeastOne<{ + /** Accessible name for the radio group. */ + 'aria-label'?: string; + /** References an element that labels the radio group. */ + 'aria-labelledby'?: string; + }> & { + /** Currently selected radio card value. */ + value?: NSRadioCards.Value; + /** Called when the selected radio card changes. */ + onChange?: (value: NSRadioCards.Value, event?: React.SyntheticEvent) => void; + /** Disables the entire radio group and all radio cards. */ + disabled?: boolean; + }; + type DefaultProps = { + defaultValue: ''; + }; + type Handlers = { + value: NSRadioCards.Value; + }; + + namespace Item { + type Props = NSFlex.Props & { + /** Disables radio card. */ + disabled?: boolean; + /** Optional icon displayed before the content. */ + iconAddon?: React.ReactNode; + /** Optional text displayed in the header. */ + text?: string; + /** Unique value associated with the radio card. */ + value: NSRadioCards.Value; + /** Additional text displayed in the header. */ + textAddon?: string; + /** Description displayed below the header. */ + description?: string; + /** Shows a dot indicator with the specified accessible label. */ + dot?: string; + }; + + type Component = Intergalactic.Component<'button', Props>; + } + + type Component = Intergalactic.Component<'div', Props> & { + Item: Item.Component; + }; +} + +export type { NSRadioCards }; diff --git a/semcore/radio-cards/src/index.ts b/semcore/radio-cards/src/index.ts new file mode 100644 index 0000000000..6fb1a9626e --- /dev/null +++ b/semcore/radio-cards/src/index.ts @@ -0,0 +1,2 @@ +export { default } from './RadioCards'; +export * from './RadioCards.type'; diff --git a/semcore/radio-cards/src/styles/radio-cards.shadow.css b/semcore/radio-cards/src/styles/radio-cards.shadow.css new file mode 100644 index 0000000000..726bd318da --- /dev/null +++ b/semcore/radio-cards/src/styles/radio-cards.shadow.css @@ -0,0 +1,50 @@ +SRadioCards { + gap: var(--intergalactic-spacing-content-gap-large, 8px); +} + +SRadioItem { + position: relative; + flex-direction: column; + flex: 1 0 0; + gap: var(--intergalactic-spacing-content-gap-small, 4px); + padding: var(--intergalactic-spacing-content-padding-medium, 12px) var(--intergalactic-spacing-content-padding-xlarge, 20px); + align-items: flex-start; + + border-radius: var(--intergalactic-pills-rounded, 6px); + background: var(--intergalactic-control-presets-bg, oklch(1 0 0)); + box-shadow: var(--intergalactic-box-shadow-control-elevated, 0px 0px 1px 0px oklch(0.176 0.033 175.7 / 0.07), 0px 1px 3px 0px oklch(0.176 0.033 175.7 / 0.07)); + cursor: pointer; + border: 1px solid transparent; + + &:hover { + border: 1px solid var(--intergalactic-control-presets-border-hover, oklch(0.137 0.026 175.7 / 0.161)); + } + + &[aria-checked='true'] { + border: 1px solid var(--intergalactic-control-presets-border-selected, oklch(0.23 0.01 140)); + } + + &[aria-disabled='true'] { + pointer-events: none; + opacity: var(--intergalactic-disabled-opacity, 0.4); + } +} + +SRadioItemHeader { + gap: var(--intergalactic-spacing-content-gap-medium, 6px); +} + +SRadioItemHeaderText, +SRadioItemDescription { + text-align: left; +} + +SRadioItemHeaderLeftAddon { + transform: translateY(4px); + color: var(--intergalactic-icon-primary-neutral, oklch(0.092 0.024 152.2 / 0.526)); +} + + +SRadioItemHeaderRightAddon { + color: var(--intergalactic-text-secondary, oklch(0.088 0.026 147.7 / 0.583)); +} diff --git a/semcore/radio-cards/tsconfig.json b/semcore/radio-cards/tsconfig.json new file mode 100644 index 0000000000..3ed2a1cc8b --- /dev/null +++ b/semcore/radio-cards/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"], + "compilerOptions": { + "rootDir": "./src" + } +} diff --git a/semcore/radio-cards/vite.config.ts b/semcore/radio-cards/vite.config.ts new file mode 100644 index 0000000000..90f46b4cb2 --- /dev/null +++ b/semcore/radio-cards/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, mergeConfig } from 'vite'; + +import viteConfig from '../../commonVite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + build: { + lib: { + entry: './src/index.ts', + }, + rollupOptions: { + external: ['react', 'react-dom', 'react/jsx-runtime', /@babel\/runtime\/*/, /@semcore\/*/], + }, + }, + }), +); diff --git a/semcore/radio-cards/vitest.config.mts b/semcore/radio-cards/vitest.config.mts new file mode 100644 index 0000000000..36006d4f8e --- /dev/null +++ b/semcore/radio-cards/vitest.config.mts @@ -0,0 +1,17 @@ +import { resolve as resolvePath } from 'node:path'; + +import { defineConfig } from 'vitest/config'; + +import { vitestPlugins, vitestResolve } from '../../vitest.config.mts'; + +export default defineConfig({ + plugins: vitestPlugins, + resolve: vitestResolve, + test: { + include: [ + '__tests__/**/*.test.tsx', + ], + environment: 'jsdom', + setupFiles: [resolvePath(__dirname, '../../tools/testing-utils/setupTests')], + }, +}); diff --git a/stories/components/pills/docs/Pills.stories.tsx b/stories/components/pills/docs/Pills.stories.tsx index 72fd715403..3d811cddcf 100644 --- a/stories/components/pills/docs/Pills.stories.tsx +++ b/stories/components/pills/docs/Pills.stories.tsx @@ -2,7 +2,6 @@ import Pills from '@semcore/ui/pills'; import type { Meta, StoryObj } from '@storybook/react-vite'; import BasicUsageExample, { defaultProps as BasicUsageProps } from './examples/basic_example'; -import CustomPillsExample, { defaultProps as CustomPillsProps } from './examples/custom_pills_example'; import TabsExample, { defaultProps as TabExampleProps } from './examples/tabs_example'; const meta: Meta = { @@ -47,21 +46,3 @@ export const Tabs: StoryObj = { }, args: TabExampleProps, }; - -export const CustomPills: StoryObj = { - render: CustomPillsExample, - argTypes: { - size: { - control: { type: 'select' }, - options: ['m', 'l'], - }, - behavior: { - control: { type: 'select' }, - options: ['manual', 'auto'], - }, - disabled: { - control: { type: 'boolean' }, - }, - }, - args: CustomPillsProps, -}; diff --git a/stories/components/pills/docs/examples/custom_pills_example.tsx b/stories/components/pills/docs/examples/custom_pills_example.tsx deleted file mode 100644 index 39067fc765..0000000000 --- a/stories/components/pills/docs/examples/custom_pills_example.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Flex } from '@semcore/ui/base-components'; -import Pills from '@semcore/ui/pills'; -import type { NSPills } from '@semcore/ui/pills'; -import { Text } from '@semcore/ui/typography'; -import React from 'react'; -import type { CSSProperties } from 'react'; - -const contentBLocks = [ - 'Only awake for snacks and existential crises. The rest of the time, she is contemplating the void from under a blanket.', - 'A yoga master: sleeps upside down without dropping a single thought. Scientists are still trying to understand how her brain stays so calm.', - 'Sleep is her full-time job. Occasionally wakes up just to sigh and go back to bed. Her alarm clock gave up and filed for early retirement.', -]; - -const pillStyles: CSSProperties = { - height: 'fit-content', - alignItems: 'stretch', - textAlign: 'inherit', - whiteSpace: 'normal', -} as const; - -type PillExampleProps = NSPills.Props & NSPills.Pill.Props; -const Demo = (props: PillExampleProps) => { - const [tab, setTab] = React.useState(0); - - return ( - - - Average sleep per day - - - - - Wombat - 16 hours - - - - - Bat - 20 hours - - - - - Koala - 22 hours - - - - - {contentBLocks[tab]} - - - ); -}; - -export const defaultProps: PillExampleProps = { - size: 'm', - disabled: undefined, - behavior: 'manual', -}; - -Demo.defaultProps = defaultProps; - -export default Demo; diff --git a/stories/components/radio-cards/docs/examples/advanced.tsx b/stories/components/radio-cards/docs/examples/advanced.tsx new file mode 100644 index 0000000000..3e44ae3300 --- /dev/null +++ b/stories/components/radio-cards/docs/examples/advanced.tsx @@ -0,0 +1,41 @@ +import FileExport from '@semcore/icon/FileExport/m'; +import Info from '@semcore/icon/Info/m'; +import { Flex } from '@semcore/ui/base-components'; +import Dot from '@semcore/ui/dot'; +import RadioCards from '@semcore/ui/radio-cards'; +import Spin from '@semcore/ui/spin'; +import Tag, { TagContainer } from '@semcore/ui/tag'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +const Demo = () => { + return ( + + + + + Custom layout + + + Combine any addons and content + 12 + + + + + Loading state + + + + One + + + + + Any children are supported + + + ); +}; + +export default Demo; diff --git a/stories/components/radio-cards/docs/examples/basic.tsx b/stories/components/radio-cards/docs/examples/basic.tsx new file mode 100644 index 0000000000..6713e777b6 --- /dev/null +++ b/stories/components/radio-cards/docs/examples/basic.tsx @@ -0,0 +1,35 @@ +import Fire from '@semcore/icon/Fire/m'; +import RadioCards from '@semcore/ui/radio-cards'; +import React, { useState } from 'react'; + +const Demo = () => { + const [value, setValue] = useState('all'); + + return ( + + + } + /> + + + + ); +}; + +export default Demo; diff --git a/stories/components/radio-cards/docs/radio-cards.stories.tsx b/stories/components/radio-cards/docs/radio-cards.stories.tsx new file mode 100644 index 0000000000..96649f2cd7 --- /dev/null +++ b/stories/components/radio-cards/docs/radio-cards.stories.tsx @@ -0,0 +1,20 @@ +import RadioCards from '@semcore/ui/radio-cards'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import AdvancedExample from './examples/advanced'; +import BasicExample from './examples/basic'; + +const meta: Meta = { + title: 'Components/RadioCards/Documentation', + component: RadioCards, +}; + +export default meta; + +export const BasicRadioCardsExample: StoryObj = { + render: BasicExample, +}; + +export const AdvancedRadioCardsExample: StoryObj = { + render: AdvancedExample, +}; diff --git a/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx b/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx new file mode 100644 index 0000000000..c847ad5be0 --- /dev/null +++ b/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx @@ -0,0 +1,95 @@ +import Fire from '@semcore/icon/Fire/m'; +import ThumbUp from '@semcore/icon/ThumbUp/m'; +import { Flex } from '@semcore/ui/base-components'; +import RadioCards from '@semcore/ui/radio-cards'; +import type { NSRadioCards } from '@semcore/ui/radio-cards'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +type ExampleProps = NSRadioCards.Props & { + disabledCard?: 'none' | '1' | '2' | '3' | '4'; + text?: string; + textAddon?: string; + description?: string; + showIconAddon?: boolean; + dot?: boolean; +}; + +const Demo = (props: ExampleProps) => { + const { + value, + disabled, + disabledCard = 'none', + text = 'Lost and Vital', + textAddon = '24', + description = 'Restore these backlinks first', + showIconAddon = true, + 'aria-label': ariaLabel = 'Radio cards', + dot = false, + } = props; + + const [selected, setSelected] = React.useState(value); + + React.useEffect(() => { + setSelected(value); + }, [value]); + + const handleChange: NSRadioCards.Props['onChange'] = (newValue, event) => { + setSelected(newValue); + props.onChange?.(newValue, event); + }; + + return ( + + + + } + disabled={disabledCard === '2'} + /> + + : undefined} + disabled={disabledCard === '4'} + /> + + Selected value: {selected || '(none)'} + + ); +}; + +export const defaultRadioCardsProps: ExampleProps = { + 'aria-label': 'Radio cards', + 'value': '2', + 'disabled': false, + 'disabledCard': '3', + 'text': 'Lost and Vital', + 'textAddon': '24', + 'description': 'Restore these backlinks first', + 'showIconAddon': true, + 'dot': false, +}; + +Demo.defaultProps = defaultRadioCardsProps; + +export default Demo; diff --git a/stories/components/radio-cards/tests/radio-cards.stories.tsx b/stories/components/radio-cards/tests/radio-cards.stories.tsx new file mode 100644 index 0000000000..dcbbbfaa06 --- /dev/null +++ b/stories/components/radio-cards/tests/radio-cards.stories.tsx @@ -0,0 +1,56 @@ +import RadioCards from '@semcore/ui/radio-cards'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import RadioCardAllPropsExample, { defaultRadioCardsProps } from './examples/radio-card-all-props'; + +const meta: Meta = { + title: 'Components/RadioCards/Tests', + component: RadioCards, +}; + +export default meta; + +export const RadioCardAllProps: StoryObj = { + render: RadioCardAllPropsExample, + argTypes: { + 'aria-label': { + control: { type: 'text' }, + description: 'Accessible name for the radio group', + }, + 'value': { + control: { type: 'select' }, + options: ['1', '2', '3', '4'], + description: 'Currently selected radio card value (group)', + }, + 'disabled': { + control: { type: 'boolean' }, + description: 'Disables the entire radio group and all radio cards', + }, + 'disabledCard': { + control: { type: 'select' }, + options: ['none', '1', '2', '3', '4'], + description: 'Disables the card with this value, to demonstrate item-level disabled (RadioCards.Item disabled prop)', + }, + 'text': { + control: { type: 'text' }, + description: 'Text of the 4th card (RadioCards.Item text prop)', + }, + 'textAddon': { + control: { type: 'text' }, + description: 'Text addon of the 4th card (RadioCards.Item textAddon prop)', + }, + 'description': { + control: { type: 'text' }, + description: 'Description of the 4th card (RadioCards.Item description prop)', + }, + 'showIconAddon': { + control: { type: 'boolean' }, + description: 'Toggles iconAddon on the 4th card (RadioCards.Item iconAddon prop)', + }, + 'dot': { + control: { type: 'boolean' }, + description: 'Shows a Dot indicator with the "New" label on the 1st card (RadioCards.Item dot prop)', + }, + }, + args: defaultRadioCardsProps, +}; diff --git a/website/docs/.vitepress/sidebarConfig.ts b/website/docs/.vitepress/sidebarConfig.ts index a5f9562fe8..b367fece4f 100644 --- a/website/docs/.vitepress/sidebarConfig.ts +++ b/website/docs/.vitepress/sidebarConfig.ts @@ -354,6 +354,11 @@ export const sideBarConfig: SidebarConfig = [ activeMatch: '/components/radio/', text: 'Radio', }, + { + link: '/components/radio-cards/radio-cards', + activeMatch: '/components/radio-cards/', + text: 'RadioCards', + }, { link: '/components/scroll-area/scroll-area', activeMatch: '/components/scroll-area/', diff --git a/website/docs/components/pills/pills-code.md b/website/docs/components/pills/pills-code.md index 7106877072..11ec92d6df 100644 --- a/website/docs/components/pills/pills-code.md +++ b/website/docs/components/pills/pills-code.md @@ -27,15 +27,3 @@ Don't forget to add role `tabpanel` and `aria-labelledby` to the content of each ::: - -## Custom pills - -You can place additional information like some metrics inside `Pills.Item`. In this case, we recommend using Pills strictly as **tabs**. - -::: sandbox - - - -::: diff --git a/website/docs/components/radio-cards/radio-cards-a11y.md b/website/docs/components/radio-cards/radio-cards-a11y.md new file mode 100644 index 0000000000..2f5dcc6ae7 --- /dev/null +++ b/website/docs/components/radio-cards/radio-cards-a11y.md @@ -0,0 +1,46 @@ +--- +title: Radio Cards +fileSource: radio-cards +tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog') +--- + +## What component has + +Component `RadioCards` functions as a group of radio buttons. + +### Keyboard support + +Table: Keyboard support + +| Key | Function | +|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Tab` | When focus moves to the `RadioCards`, it moves to the active `RadioCards.Item`. | +| `Left Arrow` , `Right Arrow` | Set `checked` value to the next/previous button in the group. If focus is on the last/first button, moves focus to the first/last button respectively. | + +See detailed information about the keyboard support for clickable elements in the [Keyboard control guide](/core-principles/a11y/a11y-keyboard#any-other-controls-filtertrigger-pills-tabline-etc). + +### Roles & attributes + +The following list describes roles and attributes that the component with already has. + +Table: Roles & attributes + +| Component / Element | Roles & attributes | Usage | +|---------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `RadioCards` | `radiogroup` | Identifies the `div` element as a container for a group of radio buttons. Isn’t focusable because focus is managed using a roving tabindex strategy as described below. | +| `RadioCards.Item` | `radio` | Identifies the `div` element as an ARIA radio button. The accessible name is computed from the child text content of the `div` element. | +| | `tabIndex="0"` | Makes the radio button focusable and includes it in the page `Tab` sequence. Set on only one radio in the radio group. Moves with focus inside the radio group so the most recently focused radio button is included in the page `Tab` sequence. This approach to managing focus is described in the section on [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd-roving-tabindex). | +| | `tabindex="-1"` | Makes the element focusable but not part of the page `Tab` sequence. Applied to all radio buttons contained in the radio group except for one that's included in the page `Tab` sequence. This approach to managing focus is described in the section on [Roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardnavigationinsidecomponents). | +| | `aria-checked="true/false"` | Identifies whether radio buttons are checked or unchecked. | | + +## Considerations for developers + +Since `RadioCards` behaves as a radio group, do not forget to add the `aria-labelledby` or `aria-label` attribute to the root element. + +## Resources + +- [A11y style guide](https://a11y-style-guide.com/style-guide/section-forms.html#kssref-forms-radio-buttons) gives core recommendations for the accessible radio buttons. + +## Other recommendations + +Find more accessibility recommendations in the common [Accessibility guide](/core-principles/a11y/a11y). diff --git a/website/docs/components/radio-cards/radio-cards-api.md b/website/docs/components/radio-cards/radio-cards-api.md new file mode 100644 index 0000000000..ebe37f4880 --- /dev/null +++ b/website/docs/components/radio-cards/radio-cards-api.md @@ -0,0 +1,25 @@ +--- +title: Radio Cards +fileSource: radio-cards +tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog') +--- + +## RadioCards + +```jsx +import RadioCards from '@semcore/ui/radio-cards'; +; +``` + + + +## RadioCards.Item + +```jsx +import RadioCards from '@semcore/ui/radio-cards'; +; +``` + + + + diff --git a/website/docs/components/radio-cards/radio-cards-changelog.md b/website/docs/components/radio-cards/radio-cards-changelog.md new file mode 100644 index 0000000000..6df201a9cc --- /dev/null +++ b/website/docs/components/radio-cards/radio-cards-changelog.md @@ -0,0 +1,7 @@ +--- +title: Radio Cards +fileSource: radio-cards +tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog') +--- + +::: changelog radio-cards ::: diff --git a/website/docs/components/radio-cards/radio-cards-code.md b/website/docs/components/radio-cards/radio-cards-code.md new file mode 100644 index 0000000000..9b5edb9ebe --- /dev/null +++ b/website/docs/components/radio-cards/radio-cards-code.md @@ -0,0 +1,25 @@ +--- +title: Radio Cards +fileSource: radio-cards +tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog') +--- + +## Basic usage + +::: sandbox + + + +::: + +## Advanced usage + +::: sandbox + + + +::: \ No newline at end of file diff --git a/website/docs/components/radio-cards/radio-cards.md b/website/docs/components/radio-cards/radio-cards.md new file mode 100644 index 0000000000..5ad38b27c6 --- /dev/null +++ b/website/docs/components/radio-cards/radio-cards.md @@ -0,0 +1,7 @@ +--- +title: Radio Cards +fileSource: radio-cards +tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog') +--- + +TODO: Needs to be filled. \ No newline at end of file diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json index 466b219300..3c0090ca7d 100644 --- a/website/docs/style/design-tokens/design-tokens.json +++ b/website/docs/style/design-tokens/design-tokens.json @@ -1735,6 +1735,7 @@ "drag-and-drop", "feature-highlight", "notice", + "radio-cards", "typography" ] }, @@ -2062,19 +2063,25 @@ "name": "--intergalactic-control-presets-bg", "value": "oklch(1 0 0)", "description": "Background of the Presets items.", - "components": [] + "components": [ + "radio-cards" + ] }, { "name": "--intergalactic-control-presets-border-selected", "value": "oklch(0.23 0.01 140)", "description": "Border color of the selected Preset item.", - "components": [] + "components": [ + "radio-cards" + ] }, { "name": "--intergalactic-control-presets-border-hover", "value": "oklch(0.137 0.026 175.7 / 0.161)", "description": "Border color of the hovered Preset item.", - "components": [] + "components": [ + "radio-cards" + ] }, { "name": "--intergalactic-progress-bar-bg", @@ -3017,6 +3024,7 @@ "flags", "fullscreen-modal", "pills", + "radio-cards", "slider", "switch", "tab-panel", @@ -3155,6 +3163,7 @@ "link", "pills", "radio", + "radio-cards", "slider", "switch", "tab-line", @@ -3184,6 +3193,7 @@ "description": "Shadow of the Switch toggle.", "components": [ "pills", + "radio-cards", "switch" ] }, @@ -3355,6 +3365,7 @@ "input-tags", "modal", "pills", + "radio-cards", "textarea", "time-picker", "tooltip" @@ -3376,7 +3387,8 @@ "description": "Extra large padding for content inside controls and surfaces.", "components": [ "bulk-textarea", - "card" + "card", + "radio-cards" ] }, { @@ -3412,6 +3424,7 @@ "dropdown-menu", "inline-input", "pagination", + "radio-cards", "tag" ] }, @@ -3428,6 +3441,7 @@ "input", "pills", "radio", + "radio-cards", "select" ] }, @@ -3443,6 +3457,7 @@ "notice-bubble", "pagination", "radio", + "radio-cards", "select", "tag" ] @@ -3570,7 +3585,8 @@ "value": "6px", "description": "Use for rounding Pills.", "components": [ - "pills" + "pills", + "radio-cards" ] }, {