diff --git a/README.md b/README.md index 90701588f89..d3f6b591f9c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A React implementation of Spectrum, Adobe’s design system. Spectrum provides a ### React Aria -A library of React Hooks that provides accessible UI primitives for your design system. +A library of unstyled React components and hooks that helps you build accessible, high quality UI components for your application or design system. [Learn more about React Aria](https://react-spectrum.adobe.com/react-aria/index.html) @@ -38,7 +38,7 @@ A collection of framework-agnostic internationalization libraries for the web. React Spectrum includes several libraries, which you can choose depending on your usecase. * [React Spectrum](https://react-spectrum.adobe.com/react-spectrum/getting-started.html) is an implementation of Adobe's design system. If you’re integrating with Adobe software or would like a complete component library to use in your project, look no further! -* [React Aria](https://react-spectrum.adobe.com/react-aria/getting-started.html) is a collection of React Hooks that provides accessible UI primitives for use in your own design system. If you're building a component library for the web from scratch with your own styling, start here. +* [React Aria](https://react-spectrum.adobe.com/react-aria/getting-started.html) is a collection of unstyled React components and hooks that helps you build accessible, high quality UI components for your own application or design system. If you're building a component library for the web from scratch with your own styling, start here. * [React Stately](https://react-spectrum.adobe.com/react-stately/getting-started.html) is a library of state management hooks for use in your component library. If you're using React Aria, you'll likely also use React Stately, but it can also be used independently (e.g. on other platforms like React Native). [Read more about our architecture](https://react-spectrum.adobe.com/architecture.html). diff --git a/package.json b/package.json index b228c25b4b4..65de1b3ae92 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "jest-matchmedia-mock": "^1.1.0", "lerna": "^3.13.2", "lfcdn": "^0.4.2", + "lucide-react": "^0.294.0", "md5": "^2.2.1", "npm-cli-login": "^1.0.0", "nyc": "^10.2.0", @@ -166,6 +167,7 @@ "sharp": "^0.31.2", "sinon": "^7.3.1", "storybook-dark-mode": "^1.1.1-canary.120.3843.0", + "tailwind-variants": "^0.1.18", "tailwindcss": "^3.2.2", "tailwindcss-animate": "^1.0.7", "tempy": "^0.5.0", @@ -189,7 +191,10 @@ "@parcel/transformer-css": { "cssModules": { "global": true, - "exclude": ["**/*.global.css", "packages/@react-aria/example-theme/**"] + "exclude": [ + "**/*.global.css", + "packages/@react-aria/example-theme/**" + ] }, "drafts": { "nesting": true diff --git a/packages/@internationalized/number/src/NumberFormatter.ts b/packages/@internationalized/number/src/NumberFormatter.ts index 5d0ade8f6a6..920a86e8cec 100644 --- a/packages/@internationalized/number/src/NumberFormatter.ts +++ b/packages/@internationalized/number/src/NumberFormatter.ts @@ -145,8 +145,11 @@ export class NumberFormatter implements Intl.NumberFormat { function getCachedNumberFormatter(locale: string, options: NumberFormatOptions = {}): Intl.NumberFormat { let {numberingSystem} = options; - if (numberingSystem && locale.indexOf('-u-nu-') === -1) { - locale = `${locale}-u-nu-${numberingSystem}`; + if (numberingSystem && locale.includes('-nu-')) { + if (!locale.includes('-u-')) { + locale += '-u-'; + } + locale += `-nu-${numberingSystem}`; } if (options.style === 'unit' && !supportsUnit) { diff --git a/packages/@react-aria/dnd/src/useAutoScroll.ts b/packages/@react-aria/dnd/src/useAutoScroll.ts index 21f1ac91859..d25eb3bafee 100644 --- a/packages/@react-aria/dnd/src/useAutoScroll.ts +++ b/packages/@react-aria/dnd/src/useAutoScroll.ts @@ -17,9 +17,14 @@ const AUTOSCROLL_AREA_SIZE = 20; export function useAutoScroll(ref: RefObject) { let scrollableRef = useRef(null); + let scrollableX = useRef(true); + let scrollableY = useRef(true); useEffect(() => { if (ref.current) { scrollableRef.current = isScrollable(ref.current) ? ref.current : getScrollParent(ref.current); + let style = window.getComputedStyle(scrollableRef.current); + scrollableX.current = /(auto|scroll)/.test(style.overflowX); + scrollableY.current = /(auto|scroll)/.test(style.overflowY); } }, [ref]); @@ -40,8 +45,12 @@ export function useAutoScroll(ref: RefObject) { }, [state]); let scroll = useCallback(() => { - scrollableRef.current.scrollLeft += state.dx; - scrollableRef.current.scrollTop += state.dy; + if (scrollableX.current) { + scrollableRef.current.scrollLeft += state.dx; + } + if (scrollableY.current) { + scrollableRef.current.scrollTop += state.dy; + } if (state.timer) { state.timer = requestAnimationFrame(scroll); diff --git a/packages/@react-aria/dnd/test/useDroppableCollection.test.js b/packages/@react-aria/dnd/test/useDroppableCollection.test.js index 81d184294b9..ceb8a814894 100644 --- a/packages/@react-aria/dnd/test/useDroppableCollection.test.js +++ b/packages/@react-aria/dnd/test/useDroppableCollection.test.js @@ -216,7 +216,7 @@ describe('useDroppableCollection', () => { } let tree = render(<> - + ); let draggable = tree.getByText('Drag me'); diff --git a/packages/dev/docs/package.json b/packages/dev/docs/package.json index 2e3fea06b7f..27460db82f1 100644 --- a/packages/dev/docs/package.json +++ b/packages/dev/docs/package.json @@ -34,5 +34,8 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-lowlight": "^2.0.0" + }, + "alias": { + "tailwind-starter": "../../../starters/tailwind/src" } } diff --git a/packages/dev/docs/pages/assets/ReactAriaOpenGraph.webp b/packages/dev/docs/pages/assets/ReactAriaOpenGraph.webp new file mode 100644 index 00000000000..e93039f111d Binary files /dev/null and b/packages/dev/docs/pages/assets/ReactAriaOpenGraph.webp differ diff --git a/packages/dev/docs/pages/assets/iphone-frame.webp b/packages/dev/docs/pages/assets/iphone-frame.webp new file mode 100644 index 00000000000..d49da3b8d48 Binary files /dev/null and b/packages/dev/docs/pages/assets/iphone-frame.webp differ diff --git a/packages/dev/docs/pages/assets/iphone-mask.webp b/packages/dev/docs/pages/assets/iphone-mask.webp new file mode 100644 index 00000000000..b56d2682d7f Binary files /dev/null and b/packages/dev/docs/pages/assets/iphone-mask.webp differ diff --git a/packages/dev/docs/pages/index.mdx b/packages/dev/docs/pages/index.mdx index ba4eea17ec9..e6b61fee927 100644 --- a/packages/dev/docs/pages/index.mdx +++ b/packages/dev/docs/pages/index.mdx @@ -58,7 +58,7 @@ A collection of libraries and tools that help you build adaptive, accessible, an }, { title: 'React Aria', - description: 'A library of React Hooks that provides accessible UI primitives for your design system.', + description: 'A library of unstyled React components and hooks that helps you build accessible, high quality UI components for your application or design system.', url: './react-aria/index.html', urlText: <>Learn more about React Aria }, diff --git a/packages/dev/docs/pages/react-aria/.postcssrc b/packages/dev/docs/pages/react-aria/.postcssrc new file mode 100644 index 00000000000..0985cb2aab0 --- /dev/null +++ b/packages/dev/docs/pages/react-aria/.postcssrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "tailwindcss": {} + } +} diff --git a/packages/dev/docs/pages/react-aria/components.mdx b/packages/dev/docs/pages/react-aria/components.mdx new file mode 100644 index 00000000000..e4b60684038 --- /dev/null +++ b/packages/dev/docs/pages/react-aria/components.mdx @@ -0,0 +1,428 @@ +{/* Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '@react-spectrum/docs'; +export default Layout; + +import styles from '@react-spectrum/docs/src/docs.css'; + +import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard'; +import ComboBoxAnatomy from '../assets/component-illustrations/ComboBox.svg'; +import SelectAnatomy from '../assets/component-illustrations/Picker.svg'; +import Button from '../assets/component-illustrations/Button.svg'; +import ToggleButton from '../assets/component-illustrations/ToggleButton.svg'; +import Checkbox from '../assets/component-illustrations/Checkbox.svg'; +import CheckboxGroup from '../assets/component-illustrations/CheckboxGroup.svg'; +import RadioGroup from '../assets/component-illustrations/RadioGroup.svg'; +import Switch from '../assets/component-illustrations/Switch.svg'; +import TextField from '../assets/component-illustrations/TextField.svg'; +import NumberField from '../assets/component-illustrations/NumberField.svg'; +import SearchField from '../assets/component-illustrations/SearchField.svg'; +import Meter from '../assets/component-illustrations/Meter.svg'; +import ProgressBar from '../assets/component-illustrations/ProgressCircle.svg'; +import Tabs from '../assets/component-illustrations/Tabs.svg'; +import Link from '../assets/component-illustrations/Link.svg'; +import Breadcrumbs from '../assets/component-illustrations/Breadcrumbs.svg'; +import Slider from '../assets/component-illustrations/Slider.svg'; +import Dialog from '../assets/component-illustrations/Dialog.svg'; +import Tooltip from '../assets/component-illustrations/Tooltip.svg'; +import Popover from '../assets/component-illustrations/Popover.svg'; +import Menu from '../assets/component-illustrations/Menu.svg'; +import ListBox from '../assets/component-illustrations/ListBox.svg'; +import ListView from '../assets/component-illustrations/ListView.svg'; +import Table from '../assets/component-illustrations/Table.svg'; +import Calendar from '../assets/component-illustrations/Calendar.svg'; +import RangeCalendar from '../assets/component-illustrations/RangeCalendar.svg'; +import DateField from '../assets/component-illustrations/DateField.svg'; +import TimeField from '../assets/component-illustrations/TimeField.svg'; +import DatePicker from '../assets/component-illustrations/DatePicker.svg'; +import DateRangePicker from '../assets/component-illustrations/DateRangePicker.svg'; +import Press from '../assets/component-illustrations/usePress.svg'; +import LongPress from '../assets/component-illustrations/useLongPress.svg'; +import Hover from '../assets/component-illustrations/useHover.svg'; +import Move from '../assets/component-illustrations/useMove.svg'; +import Keyboard from '../assets/component-illustrations/useKeyboard.svg'; +import FocusRing from '../assets/component-illustrations/useFocusRing.svg'; +import FocusScope from '../assets/component-illustrations/FocusScope.svg'; +import FocusWithin from '../assets/component-illustrations/useFocusWithin.svg'; +import Focus from '../assets/component-illustrations/useFocus.svg'; +import TagGroup from '../assets/component-illustrations/TagGroup.svg'; +import DropZone from '../assets/component-illustrations/DropZone.svg'; +import FileTrigger from '../assets/component-illustrations/FileTrigger.svg' +import Form from '../assets/component-illustrations/Form.svg' + +--- +category: Introduction +navigationTitle: Components +description: Craft world-class accessible components with custom styles. +order: 5 +--- + +# React Aria Components + +## Buttons + +
+ + +
+ +## Pickers + +
+ + + + + + + + + +
+ +## Collections + +
+ + + + + + + + + + + + + + + + + + + + + + + +## Date and time + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## Overlays + +
+ + + + + + + + + + + + + +
+ +## Forms + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +## Navigation + +
+ + + + + + + + + + + + + +
+ +## Status + +
+ + + + + + + + + +
+ +## Drag and drop + +
+ + + + + +
+ +## Interactions + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/packages/dev/docs/pages/react-aria/home.global.css b/packages/dev/docs/pages/react-aria/home.global.css new file mode 100644 index 00000000000..9168fc1a60c --- /dev/null +++ b/packages/dev/docs/pages/react-aria/home.global.css @@ -0,0 +1,513 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +@config "./tailwind.config.js"; +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .focus-ring { + @apply outline outline-0 outline-blue-600 focus-visible:outline-2; + } +} + +@font-face { + font-family: "adobe-clean"; + src: url("https://use.typekit.net/af/cb695f/000000000000000000017701/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff2"),url("https://use.typekit.net/af/cb695f/000000000000000000017701/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff"),url("https://use.typekit.net/af/cb695f/000000000000000000017701/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("opentype"); + font-display: swap; + font-style: normal; + font-weight: 400; +} + +@font-face { + font-family: "adobe-clean"; + src: url("https://use.typekit.net/af/74ffb1/000000000000000000017702/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i4&v=3") format("woff2"),url("https://use.typekit.net/af/74ffb1/000000000000000000017702/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i4&v=3") format("woff"),url("https://use.typekit.net/af/74ffb1/000000000000000000017702/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i4&v=3") format("opentype"); + font-display: swap; + font-style: italic; + font-weight: 400; +} + +@font-face { + font-family: "adobe-clean"; + src: url("https://use.typekit.net/af/eaf09c/000000000000000000017703/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("woff2"),url("https://use.typekit.net/af/eaf09c/000000000000000000017703/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("woff"),url("https://use.typekit.net/af/eaf09c/000000000000000000017703/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("opentype"); + font-display: swap; + font-style: normal; + font-weight: 700; +} + +@font-face { + font-family: "Source Code Pro"; + src: url("https://use.typekit.net/af/398a64/00000000000000007735dc06/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff2"),url("https://use.typekit.net/af/398a64/00000000000000007735dc06/30/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff"),url("https://use.typekit.net/af/398a64/00000000000000007735dc06/30/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("opentype"); + font-display: swap; + font-style: normal; + font-weight: 400; +} + +body { + font-family: "adobe-clean", system-ui, sans-serif; + background: var(--page-bg); + color-scheme: dark light; + @apply [--page-bg:theme(colors.zinc.50)] dark:[--page-bg:theme(colors.zinc.900)]; +} + +@media (min-resolution: 200dpi) { + html { + font-size: 18px; + } + + .text-sm { + font-size: 0.9rem; + } +} + +* { + -webkit-tap-highlight-color: transparent; +} + +.header-background { + @apply [--l:91%] [--c:0.11] dark:[--l:32%] dark:[--c:0.13]; + background: linear-gradient(to bottom, transparent 0% 80%, var(--page-bg)), + radial-gradient(circle at 50% 60%, transparent 0% 50%, var(--page-bg)), + conic-gradient(from 30deg at 50% 60%, + oklch(var(--l) var(--c) 220), + oklch(min(var(--l), 86%) var(--c) 260), + oklch(var(--l) var(--c) 300), + oklch(var(--l) var(--c) 340), + oklch(var(--l) var(--c) 0), + oklch(var(--l) var(--c) 40), + oklch(var(--l) var(--c) 60), + oklch(max(var(--l), 44%) var(--c) 100), + oklch(max(var(--l), 38%) var(--c) 140), + oklch(var(--l) var(--c) 180), + oklch(var(--l) var(--c) 220) + ); + background-repeat: no-repeat; +} + +main > section > h2 { + @apply text-4xl md:text-5xl font-semibold mb-4 pb-2 text-transparent; + + + p { + @apply text-lg md:text-2xl max-w-5xl text-slate-700 dark:text-slate-400; + text-wrap: pretty; + } +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + scrollbar-width: none; +} + +:root { + --hljs-color: theme(colors.gray.800); + --hljs-background: transparent; + --hljs-keyword-color: theme(colors.fuchsia.700); + --hljs-section-color: lch(from theme(colors.red.600) calc(l - 2%) c h); + --hljs-string-color: theme(colors.green.700); + --hljs-literal-color: theme(colors.purple.700); + --hljs-attribute-color: theme(colors.indigo.700); + --hljs-class-color: theme(colors.cyan.600); + --hljs-function-color: theme(colors.blue.600); + --hljs-variable-color: theme(colors.purple.700); + --hljs-title-color: theme(colors.indigo.700); + --hljs-comment-color: theme(colors.gray.700); + --mark-background: theme(colors.blue.400/10%); + --mark-border: theme(colors.blue.500); +} + +@media (prefers-color-scheme: dark) { + :root { + --hljs-color: theme(colors.gray.300); + --hljs-keyword-color: theme(colors.fuchsia.300); + --hljs-section-color: theme(colors.red.400); + --hljs-string-color: theme(colors.green.400); + --hljs-literal-color: theme(colors.purple.400); + --hljs-attribute-color: theme(colors.indigo.400); + --hljs-class-color: theme(colors.cyan.400); + --hljs-function-color: theme(colors.blue.400); + --hljs-variable-color: theme(colors.purple.400); + --hljs-title-color: theme(colors.indigo.400); + --hljs-comment-color: theme(colors.gray.400); + --mark-border: theme(colors.blue.400); + } +} + +pre .source { + @apply p-5 text-xs sm:text-sm min-h-[250px] h-full min-w-fit; + font-family: source-code-pro, 'Source Code Pro', Monaco, monospace; +} + +pre.large { + @apply hidden lg:block; +} + +pre.medium { + @apply hidden sm:block lg:hidden; +} + +pre.small { + @apply sm:hidden +} + +.card-shadow { + box-shadow: 0 0 2px rgb(0 0 0 / 12%), 0 3px 6px rgb(0 0 0 / 4%), 0 4px 8px 0 rgba(0 0 0 / 8%); + outline: 1px solid transparent; /* WHCM */ + @apply dark:border dark:border-zinc-200/10 dark:bg-clip-padding; +} + +.card-shadow-hover:hover { + box-shadow: 0 0 2px rgb(0 0 0 / 18%), 0 3px 8px rgb(0 0 0 / 6%), 0 4px 16px 0 rgba(0 0 0 / 10%); +} + +.card-shadow-hover:focus-visible { + @apply focus-ring; +} + +.edge-mask { + mask-image: linear-gradient(to right, transparent, white 8px calc(100% - 8px), transparent); +} + +@keyframes touch-animation { + 0% { + opacity: var(--hover-opacity); + transform: translate(10px, 135px); + } + + 15%, 16% { + opacity: var(--hover-opacity); + transform: translate(7px, 0); + } + + 17.2%, 19% { + opacity: var(--pressed-opacity); + transform: translate(7px, 0); + } + + 25%, 27% { + opacity: var(--pressed-opacity); + transform: translate(7px, 48px); + } + + 35%, 36% { + opacity: var(--pressed-opacity); + transform: translate(7px, 7px); + } + + 37.2% { + opacity: var(--hover-opacity); + transform: translate(7px, 7px); + } + + 50%, 55% { + opacity: var(--hover-opacity); + transform: translate(4px, 52px); + } + + 65%, 66% { + opacity: var(--hover-opacity); + transform: translate(7px, 0); + } + + 67.2%, 69% { + opacity: var(--pressed-opacity); + transform: translate(7px, 0); + } + + 75%, 77% { + opacity: var(--pressed-opacity); + transform: translate(7px, 48px); + } + + 85%, 86% { + opacity: var(--pressed-opacity); + transform: translate(7px, 7px); + } + + 87.2% { + opacity: var(--hover-opacity); + transform: translate(7px, 7px); + } + + 100% { + opacity: var(--hover-opacity); + transform: translate(10px, 135px); + } +} + +@keyframes switch-animation { + 0%, 16% { + @apply ml-6 w-8; + } + + 18.5%, 22% { + @apply ml-4 w-10; + } + + 25%, 30% { + @apply ml-6 w-8; + } + + 33%, 36.5% { + @apply ml-4 w-10; + } + + 38.5%, 66% { + @apply ml-0 w-8; + } + + 68.5%, 72% { + @apply ml-0 w-10; + } + + 75%, 80% { + @apply ml-0 w-8; + } + + 83%, 86.5% { + @apply ml-0 w-10; + } + + 88.5%, 100% { + @apply ml-6 w-8; + } +} + +@keyframes switch-background-animation { + 0%, 36.5% { + background: var(--bg-selected); + } + + 38.5%, 86.5% { + background: var(--bg); + } + + 88.5%, 100% { + background: var(--bg-selected); + } +} + +.iphone-frame { + background-image: url(../assets/iphone-frame.webp); + background-size: contain; +} + +.iphone-mask { + mask-image: url(../assets/iphone-mask.webp), linear-gradient(#fff 0 0); + mask-size: contain; + mask-composite: exclude; +} + +@keyframes cross-fade { + 0%, 40% { + opacity: var(--fade-from, 0); + } + + 50%, 90% { + opacity: var(--fade-to, 1); + } + + 100% { + opacity: var(--fade-from, 0); + } +} + +@keyframes highlight { + 0%, 30% { + opacity: 0; + } + + 50% { + opacity: 1; + } + + 70%, 100% { + opacity: 0; + } +} + +.cross-fade { + animation: cross-fade 5s infinite; + mix-blend-mode: plus-lighter; +} + +.highlight-tags { + .tag:nth-child(1 of .tag), + .tag:nth-child(n+4 of .tag):nth-child(-n+8 of .tag), + .tag:nth-last-child(1 of .tag) { + position: relative; + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 0.99lh; + opacity: 0; + @apply rounded bg-red-600/[15%] dark:bg-red-600/20; + animation: highlight 5s infinite var(--delay, 0s); + } + } +} + +.code-mask { + mask: linear-gradient(to bottom, white 0% 70%, transparent); +} + +.cyan-gradient-background { + --a: oklch(90% 0.05 200); + --a-shape: ellipse 30% 23% at 30% 56%; + --b: oklch(94% 0.09 175); + --b-shape: ellipse 30% 30% at 71% 42%; + --c: oklch(96% 0.06 218); + --c-shape: ellipse 40% 25% at 50% 72%; + background: radial-gradient(var(--a-shape), var(--a), transparent), + radial-gradient(var(--b-shape), var(--b), transparent), + radial-gradient(var(--c-shape), var(--c), transparent); +} + +@media (width < 768px) { + .cyan-gradient-background { + --a-shape: circle 250px at 26% 65%; + --b-shape: circle 250px at 73% 64%; + --c-shape: circle 250px at 50% 70%; + } +} + +@media (prefers-color-scheme: dark) { + .cyan-gradient-background { + --a: oklch(25% 0.1 200); + --b: oklch(27% 0.1 175); + --c: oklch(26% 0.08 218); + } +} + +.blue-gradient-background { + --a: oklch(94% 0.08 250); + --b: oklch(94% 0.12 275); + --c: oklch(91% 0.15 290); + background: radial-gradient(circle farthest-side at 28% 54%, var(--a), transparent 36%), + radial-gradient(circle farthest-side at 65% 45%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 65%, var(--c), transparent 50%); +} + +@media (prefers-color-scheme: dark) { + .blue-gradient-background { + --a: oklch(25% 0.08 250); + --b: oklch(25% 0.07 275); + --c: oklch(28% 0.1 280); + } +} + +.orange-gradient-background { + --color: oklch(96% 0.1 75); + --shape: ellipse 50% 35% at 50% 58%; + background: radial-gradient(var(--shape), var(--color), transparent); +} + +@media (width < 1080px) { + .orange-gradient-background { + --shape: ellipse 60% 40% at 50% 63%; + } +} + +@media (prefers-color-scheme: dark) { + .orange-gradient-background { + --color: oklch(27% 0.09 71); + } +} + +.red-gradient-background { + --a: oklch(96% 0.1 350); + --a-shape: ellipse 30% 30% at 29% 54%; + --b: oklch(96% 0.11 6); + --b-shape: ellipse 35% 30% at 66% 50%; + --c: oklch(96% 0.15 20); + --c-shape: ellipse 40% 25% at 50% 70%; + background: radial-gradient(var(--a-shape), var(--a), transparent), + radial-gradient(var(--b-shape), var(--b), transparent), + radial-gradient(var(--c-shape), var(--c), transparent); +} + +@media (width < 768px) { + .red-gradient-background { + --a-shape: circle 300px at 20% 72%; + --b-shape: circle 300px at 50% 72%; + --c-shape: circle 300px at 80% 72%; + } +} + +@media (prefers-color-scheme: dark) { + .red-gradient-background { + --a: oklch(27% 0.11 350); + --b: oklch(26% 0.11 6); + --c: oklch(26% 0.1 20); + } +} + +.pink-gradient-background { + --a: oklch(92% 0.11 300); + --a-shape: ellipse 30% 30% at 29% 57%; + --b: oklch(96% 0.11 320); + --b-shape: ellipse 35% 30% at 66% 47%; + --c: oklch(96% 0.14 340); + --c-shape: ellipse 40% 25% at 50% 70%; + background: radial-gradient(var(--a-shape), var(--a), transparent), + radial-gradient(var(--b-shape), var(--b), transparent), + radial-gradient(var(--c-shape), var(--c), transparent); +} + +@media (width < 768px) { + .pink-gradient-background { + --a-shape: circle 400px at 50% 40%; + --b-shape: circle 400px at 50% 60%; + --c-shape: circle 400px at 50% 80%; + } +} + +@media (prefers-color-scheme: dark) { + .pink-gradient-background { + --a: oklch(30% 0.11 300); + --b: oklch(30% 0.11 320); + --c: oklch(27% 0.1 340); + } +} + + +.green-gradient-background { + --a: oklch(96% 0.1 120); + --a-shape: ellipse 30% 30% at 29% 54%; + --b: oklch(91% 0.12 140); + --b-shape: ellipse 35% 30% at 66% 41%; + --c: oklch(94% 0.06 150); + --c-shape: ellipse 40% 25% at 50% 74%; + background: radial-gradient(var(--a-shape), var(--a), transparent), + radial-gradient(var(--b-shape), var(--b), transparent), + radial-gradient(var(--c-shape), var(--c), transparent); +} + +@media (width < 768px) { + .green-gradient-background { + --a-shape: circle 400px at 20% 63%; + --b-shape: circle 400px at 50% 63%; + --c-shape: circle 400px at 80% 63%; + } +} + +@media (prefers-color-scheme: dark) { + .green-gradient-background { + --a: oklch(28% 0.1 130); + --b: oklch(28% 0.12 145); + --c: oklch(26% 0.06 150); + } +} diff --git a/packages/dev/docs/pages/react-aria/home/A11y.tsx b/packages/dev/docs/pages/react-aria/home/A11y.tsx new file mode 100644 index 00000000000..99e03c52971 --- /dev/null +++ b/packages/dev/docs/pages/react-aria/home/A11y.tsx @@ -0,0 +1,264 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import {animate, useIntersectionObserver} from './utils'; +import {Button} from 'tailwind-starter/Button'; +import {ChevronDown, WifiIcon} from 'lucide-react'; +import {createPortal, flushSync} from 'react-dom'; +import {Finger} from './components'; +import {Key, useDateFormatter} from 'react-aria'; +import {Label} from 'tailwind-starter/Field'; +import {ListBox, Select, SelectValue} from 'react-aria-components'; +import {Popover} from 'tailwind-starter/Popover'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {SelectItem} from 'tailwind-starter/Select'; + +interface Point { + top: number, + left: number +} + +interface Rect extends Point { + width: number, + height: number +} + +const swipeRightKeyframes: PropertyIndexedKeyframes = { + transform: [ + 'translate(-80px, 0)', + 'translate(-80px, 0)', + 'translate(80px, 0)', + 'translate(80px, 0)' + ], + opacity: [ + '0', + 'var(--pressed-opacity)', + 'var(--pressed-opacity)', + '0' + ], + offset: [0, 0.2, 0.8, 1], + easing: 'ease-in-out' +}; + +const doubleTapKeyframes: PropertyIndexedKeyframes = { + opacity: [ + '0', + 'var(--pressed-opacity)', + 'var(--hover-opacity)', + 'var(--pressed-opacity)', + '0' + ], + easing: 'ease-in-out' +}; + +export function A11y() { + let ref = useRef(null); + let fingerRef = useRef(null); + let [cursorRect, setCursorRect] = useState(null); + let [fingerPos, setFingerPos] = useState(null); + let [isOpen, setOpen] = useState(false); + let [caption, setCaption] = useState(''); + let [selectedKey, setSelectedKey] = useState('read'); + useIntersectionObserver(ref, useCallback(() => { + let button: HTMLButtonElement | null = null; + let listbox: HTMLElement | null = null; + const swipeRight = { + time: 500, + perform() { + fingerRef.current!.animate(swipeRightKeyframes, {duration: 500}); + } + }; + + const doubleTap = { + time: 800, + perform() { + fingerRef.current!.animate(doubleTapKeyframes, {duration: 500}); + } + }; + + let rect = getRect(ref.current); + flushSync(() => { + setFingerPos({ + top: rect.top + rect.height - 170, + left: rect.left + rect.width / 2 - 20 + }); + }); + + let cancel = animate([ + swipeRight, + { + time: 1000, + perform() { + let label = ref.current!.querySelector('span'); + setCursorRect(getRect(label, 5)); + setCaption('Permissions'); + } + }, + swipeRight, + { + time: 2000, + perform() { + button = ref.current!.querySelector('button'); + setCursorRect(getRect(button)); + setCaption('Read Only Permissions, Pop up button, List box popup, Double tap to activate the picker'); + } + }, + doubleTap, + { + time: 180, + perform() { + setOpen(true); + } + }, + { + time: 1500, + perform() { + listbox = document.getElementById(button!.getAttribute('aria-controls')!); + let option = listbox!.querySelector('[role=option]'); + setCursorRect(getRect(option)); + setCaption('Permissions, Read Only, List start'); + } + }, + swipeRight, + { + time: 1000, + perform() { + let option = listbox!.querySelectorAll('[role=option]')[1]; + setCursorRect(getRect(option)); + setCaption('Edit'); + } + }, + swipeRight, + { + time: 1500, + perform() { + let option = listbox!.querySelectorAll('[role=option]')[2]; + setCursorRect(getRect(option)); + setCaption('Admin, List end'); + } + }, + swipeRight, + { + time: 1500, + perform() { + let option = listbox!.parentElement!.querySelector('button'); + setCursorRect(getRect(option)); + setCaption('Dismiss, button'); + } + }, + doubleTap, + { + time: 2000, + perform() { + setOpen(false); + setCursorRect(getRect(button)); + setCaption('Read Only Permissions, Pop up button, List box popup, Double tap to activate the picker'); + } + }, + { + time: 800, + perform() { + setCursorRect(null); + setCaption(''); + } + } + ]); + + return () => { + cancel(); + setOpen(false); + setCursorRect(null); + setCaption(''); + }; + }, [])); + + return ( +
+
+ + + + + + + + +
+
+ {fingerPos && createPortal(, document.body)} + {cursorRect && createPortal(( +
+ ), document.body)} + +
+ {caption &&
{caption}
} +
+ ); +} + +function Clock() { + let formatter = useDateFormatter({ + timeStyle: 'short' + }); + + let [time, setTime] = useState(() => new Date()); + + useEffect(() => { + let nextMinute = Math.floor((Date.now() + 60000) / 60000) * 60000; + let timeout = setTimeout(() => { + setTime(new Date()); + }, nextMinute - Date.now()); + + return () => clearTimeout(timeout); + }); + + return ( + + {formatter.format(time)} + + ); +} + +function getRect(element: Element | null, px = 0) { + if (!element) { + return {top: 0, left: 0, width: 0, height: 0}; + } + + let rect = element.getBoundingClientRect(); + return { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX - px, + width: rect.width + px * 2, + height: rect.height + }; +} diff --git a/packages/dev/docs/pages/react-aria/home/ExampleApp.tsx b/packages/dev/docs/pages/react-aria/home/ExampleApp.tsx new file mode 100644 index 00000000000..b27c9c190e4 --- /dev/null +++ b/packages/dev/docs/pages/react-aria/home/ExampleApp.tsx @@ -0,0 +1,541 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import {AlertDialog} from 'tailwind-starter/AlertDialog'; +import {Arrow} from './components'; +import {Button} from 'tailwind-starter/Button'; +import {Cell, Column, Row, TableHeader} from 'tailwind-starter/Table'; +import {Checkbox} from 'tailwind-starter/Checkbox'; +import {CloudSun, Dessert, Droplet, Droplets, FilterIcon, MoreHorizontal, PencilIcon, PlusIcon, RefreshCw, SlidersIcon, StarIcon, Sun, SunDim, TrashIcon} from 'lucide-react'; +import {ColumnProps, Dialog, DialogTrigger, DropZone, Form, Heading, isFileDropItem, Key, MenuTrigger, ModalOverlay, ModalOverlayProps, Modal as RACModal, ResizableTableContainer, Selection, SortDescriptor, Table, TableBody, Text, ToggleButton, ToggleButtonProps, TooltipTrigger} from 'react-aria-components'; +import {ComboBox, ComboBoxItem} from 'tailwind-starter/ComboBox'; +import {DatePicker} from 'tailwind-starter/DatePicker'; +import {focusRing} from 'tailwind-starter/utils'; +import {getLocalTimeZone, today} from '@internationalized/date'; +import {GridList, GridListItem} from 'tailwind-starter/GridList'; +import {Menu, MenuItem} from 'tailwind-starter/Menu'; +import {Modal} from 'tailwind-starter/Modal'; +import plants from './plants'; +import {Popover} from 'tailwind-starter/Popover'; +import React, {ReactElement, UIEvent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {SearchField} from 'tailwind-starter/SearchField'; +import {Select, SelectItem} from 'tailwind-starter/Select'; +import {Tag, TagGroup} from 'tailwind-starter/TagGroup'; +import {TextField} from 'tailwind-starter/TextField'; +import {Tooltip} from 'tailwind-starter/Tooltip'; +import {tv} from 'tailwind-variants'; +import {useCollator, useFilter, VisuallyHidden} from 'react-aria'; +import {useMediaQuery} from '@react-spectrum/utils'; + +type Plant = typeof plants[0] & {isFavorite: boolean}; + +const allColumns: ColumnProps[] = [ + {id: 'favorite', children: Favorite, width: 40, minWidth: 40}, + {id: 'common_name', children: 'Name', minWidth: 150, allowsSorting: true}, + {id: 'cycle', children: 'Cycle', defaultWidth: 120, allowsSorting: true}, + {id: 'sunlight', children: 'Sunlight', defaultWidth: 120, allowsSorting: true}, + {id: 'watering', children: 'Watering', defaultWidth: 120, allowsSorting: true}, + {id: 'actions', children: Actions, width: 44, minWidth: 44} +]; + +let hideOnScroll = document.getElementById('hideOnScroll'); + +export function ExampleApp() { + let [sortDescriptor, setSortDescriptor] = useState({ + column: 'common_name', + direction: 'ascending' + }); + + let [allItems, setAllItems] = useState(() => plants.map(p => ({...p, isFavorite: false}))); + let [search, setSearch] = useState(''); + let [favorite, setFavorite] = useState(false); + let [cycles, setCycles] = useState(new Set()); + let [sunlight, setSunlight] = useState(new Set()); + let [watering, setWatering] = useState(new Set()); + + let {contains} = useFilter({sensitivity: 'base'}); + let collator = useCollator(); + let dir = sortDescriptor.direction === 'descending' ? -1 : 1; + let items = allItems + .filter(item => + (contains(item.common_name, search) || contains(item.scientific_name.join(''), search)) + && (!favorite || item.isFavorite) + && (cycles === 'all' || cycles.size === 0 || cycles.has(item.cycle)) + && (sunlight === 'all' || sunlight.size === 0 || sunlight.has(getSunlight(item))) + && (watering === 'all' || watering.size === 0 || watering.has(item.watering)) + ) + .sort((a: any, b: any) => collator.compare(a[sortDescriptor.column!], b[sortDescriptor.column!]) * dir); + + let [visibleColumns, setVisibleColumns] = useState(new Set(['favorite', 'common_name', 'sunlight', 'watering', 'actions'])); + let columns = useMemo(() => { + let res = allColumns.filter(c => visibleColumns === 'all' || visibleColumns.has(c.id!)); + res[1] = {...res[1], isRowHeader: true}; + return res; + }, [visibleColumns]); + + let filters = 0; + if (favorite) { + filters++; + } + if (cycles !== 'all') { + filters += cycles.size; + } + if (sunlight !== 'all') { + filters += sunlight.size; + } + if (watering !== 'all') { + filters += watering.size; + } + + let clearFilters = () => { + setFavorite(false); + setCycles(new Set()); + setSunlight(new Set()); + setWatering(new Set()); + }; + + let toggleFavorite = (id: number, isFavorite: boolean) => { + setAllItems(allItems => { + let items = [...allItems]; + let index = items.findIndex(item => item.id === id); + items[index] = {...items[index], isFavorite}; + return items; + }); + }; + + let addItem = (item: Plant) => { + setAllItems(allItems => [...allItems, item]); + }; + + let editItem = (item: Plant) => { + setAllItems(allItems => { + let items = [...allItems]; + let index = items.findIndex(i => i.id === item.id); + items[index] = item; + return items; + }); + }; + + let deleteItem = () => { + setAllItems(allItems => { + if (!actionItem) { + return allItems; + } + + let items = [...allItems]; + let index = items.findIndex(item => item.id === actionItem!.id); + items.splice(index, 1); + return items; + }); + }; + + let [isScrolled, setScrolled] = useState(false); + let onScroll = (e: UIEvent) => { + if (hideOnScroll) { + let newIsScrolled = e.currentTarget.scrollTop > 0 || e.currentTarget.scrollLeft > 0; + if (newIsScrolled !== isScrolled) { + setScrolled(newIsScrolled); + } + } + }; + + let hasAtLeast4Items = items.length >= 4; + useEffect(() => { + if (hideOnScroll) { + hideOnScroll.style.opacity = isScrolled || !hasAtLeast4Items ? '0' : '1'; + } + }, [isScrolled, hasAtLeast4Items]); + + let [dialog, setDialog] = useState(null); + let [actionItem, setActionItem] = useState(null); + let onAction = (item: typeof items[0], action: Key) => { + switch (action) { + case 'favorite': + toggleFavorite(item.id, !item.isFavorite); + break; + default: + setDialog(action); + setActionItem(item); + break; + } + }; + + let onDialogClose = () => setDialog(null); + + let isSmall = useMediaQuery('(max-width: 640px)'); + + return ( +
+
+ + + + + Filters + + + + Filters + {filters > 0 && } +
+ Favorite + + Annual + Perennial + + + {sunIcons['full sun']} Full Sun + {sunIcons['part sun']} Part Sun + {sunIcons['part shade']} Part Shade + + + {wateringIcons['Frequent']} Frequent + {wateringIcons['Average']} Average + {wateringIcons['Minimum']} Minimum + +
+
+
+
+ + + + Columns + + + Name + Cycle + Sunlight + Watering + + + + + + + + +
+ {isSmall && + + {item => ( + +
+ + {item.common_name} + {item.scientific_name} + + + onAction(item, action)}> + {item.isFavorite ? 'Unfavorite' : 'Favorite'} + Edit… + Delete… + + +
+
+ )} +
+ } + {!isSmall && +
+ + {column => } + + + {item => ( + + {column => { + switch (column.id) { + case 'favorite': + return ( + + toggleFavorite(item.id, v)} /> + + ); + case 'common_name': + return ( + +
+ + {item.common_name} + {item.scientific_name} +
+
+ ); + case 'cycle': + return ; + case 'sunlight': + return ; + case 'watering': + return ; + case 'actions': + return ( + + + + onAction(item, action)}> + {item.isFavorite ? 'Unfavorite' : 'Favorite'} + Edit… + Delete… + + + + ); + default: + return <>; + } + }} +
+ )} +
+
+ } + + + Are you sure you want to delete "{actionItem?.common_name}"? + + + + + + + ); +} + +const labelStyles = { + gray: 'bg-gray-100 text-gray-600 border-gray-200 dark:bg-zinc-700 dark:text-zinc-300 dark:border-zinc-600', + green: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-300/20 dark:text-green-400 dark:border-green-300/10', + yellow: 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-300/20 dark:text-yellow-400 dark:border-yellow-300/10', + blue: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-400/20 dark:text-blue-300 dark:border-blue-400/10' +}; + +function Label({color, icon, children}: {color: keyof typeof labelStyles, icon: React.ReactNode, children: React.ReactNode}) { + return {icon} {children}; +} + +const cycleIcon =