diff --git a/docs/app/views/examples/components/expandable_card/_preview.html.erb b/docs/app/views/examples/components/expandable_card/_preview.html.erb index 9263c4911b..1b78064d5e 100644 --- a/docs/app/views/examples/components/expandable_card/_preview.html.erb +++ b/docs/app/views/examples/components/expandable_card/_preview.html.erb @@ -104,4 +104,74 @@ disabled: false, has_error: false } %> -<% end %> \ No newline at end of file +<% end %> + +<%= md(" +#### Custom Header + +A custom header can be applied using the `sage_expandable_card_custom_header` section. This will replace the default header with the content provided. +This should be used in conjunction with the `align_trigger` option. + +", use_sage_type: true) %> + +<%= sage_component SageExpandableCard, { + trigger_label: "Expand", + align_arrow_right: true, + align_trigger: "left", +} do %> + <% content_for :sage_expandable_card_custom_header do %> +

This is custom header content with trigger to left

+ <% end %> + <%= sage_component SageCheckbox, { + id:"c4", + label_text: "Grant offers", + checked: false, + disabled: false, + has_error: false + } %> + <%= sage_component SageCheckbox, { + id:"c5", + label_text: "Add tags", + checked: false, + disabled: false, + has_error: false + } %> + <%= sage_component SageCheckbox, { + id:"c6", + label_text: "Subscribe to emails", + checked: false, + disabled: false, + has_error: false + } %> +<% end %> + +<%= sage_component SageExpandableCard, { + trigger_label: "Expand", + align_arrow_right: true, + align_trigger: "right", +} do %> + <% content_for :sage_expandable_card_custom_header do %> +

This is custom header content with trigger to right

+ <% end %> + <%= sage_component SageCheckbox, { + id:"c4", + label_text: "Grant offers", + checked: false, + disabled: false, + has_error: false + } %> + <%= sage_component SageCheckbox, { + id:"c5", + label_text: "Add tags", + checked: false, + disabled: false, + has_error: false + } %> + <%= sage_component SageCheckbox, { + id:"c6", + label_text: "Subscribe to emails", + checked: false, + disabled: false, + has_error: false + } %> +<% end %> diff --git a/docs/app/views/examples/components/expandable_card/_props.html.erb b/docs/app/views/examples/components/expandable_card/_props.html.erb index dbd9f5f387..e5c974c152 100644 --- a/docs/app/views/examples/components/expandable_card/_props.html.erb +++ b/docs/app/views/examples/components/expandable_card/_props.html.erb @@ -4,6 +4,12 @@ <%= md('Boolean') %> <%= md('`nil`') %> + + <%= md('`align_trigger`') %> + <%= md("Aligns trigger button to left or right. Can be used in conjunction with the `sage_expandable_card_custom_header` section.") %> + <%= md('`"left"`, `"right"`') %> + <%= md('`nil`') %> + <%= md('`expanded`') %> <%= md("Adds the `sage-expandable-card--expanded` class to the block level `sage-expandable-card` and allows you to load the expandable card body expanded") %> @@ -28,3 +34,8 @@ <%= md('String') %> <%= md('`nil`') %> + + <%= md('`sage_expandable_card_custom_header`') %> + <%= md('This area can be used in order to provide custom header content. This should be used in conjunction with the `align_trigger` option.') %> + + diff --git a/docs/app/views/examples/components/form_select/_preview.html.erb b/docs/app/views/examples/components/form_select/_preview.html.erb index 33a7062412..c363e186e5 100644 --- a/docs/app/views/examples/components/form_select/_preview.html.erb +++ b/docs/app/views/examples/components/form_select/_preview.html.erb @@ -145,4 +145,34 @@ ] } %> + +

Example on how to pass additional attributes to the component

+<%= sage_component SageFormSelect, { + name: "Consoles", + component_attributes: { + onChange: "handleOnChange(this)", + "my-attribute": "hello" + }, + select_options: [ + { + text: "X-Box", + value: "x-box" + }, + { + text: "PlayStation", + value: "playstation" + }, + { + text: "Nintendo Switch", + value: "switch" + }, + ] +} %> +

Note: For a fully styled dropdown selector, see the Dropdown component.

diff --git a/docs/lib/sage_rails/app/sage_components/sage_component.rb b/docs/lib/sage_rails/app/sage_components/sage_component.rb index 884cdfd24b..dacc0489a6 100644 --- a/docs/lib/sage_rails/app/sage_components/sage_component.rb +++ b/docs/lib/sage_rails/app/sage_components/sage_component.rb @@ -7,6 +7,7 @@ class SageComponent ATTRIBUTE_SCHEMA = { test_id: [:optional, NilClass, String], + component_attributes: [:optional, NilClass, Hash], html_attributes: [:optional, NilClass, Hash], spacer: [:optional, NilClass, SageSchemas::SPACER], css_classes: [:optional, NilClass, String], @@ -17,6 +18,10 @@ def generated_css_classes @generated_css_classes ||= "" end + def generated_component_attributes + @generated_component_attributes ||= "" + end + def generated_html_attributes @generated_html_attributes ||= "" end @@ -55,6 +60,24 @@ def spacer=(spacer_hash) end end + # SageComponent Custom Event Attributes + # Accepts a :component_attributes has that generates the additional + # attributes on the element and not the parent container. + # + # USAGE: + # sage_component , { component_attributes: { "onChange": "handleChange(this)" } } + # sage_component , { component_attributes: { "onChange": "alert('hello world')" } } + def component_attributes + @component_attributes ||= {} + end + + def component_attributes=(component_attributes_hash) + @component_attributes = component_attributes_hash + component_attributes_hash.each do |key, value| + generated_component_attributes << " #{key}=\"#{value.to_s}\"" + end + end + # SageComponent Custom Html Attributes # Accepts a :html_attributes hash that generates additional html attributes for a component. # diff --git a/docs/lib/sage_rails/app/sage_components/sage_expandable_card.rb b/docs/lib/sage_rails/app/sage_components/sage_expandable_card.rb index e4df7e000a..9ab0473dec 100644 --- a/docs/lib/sage_rails/app/sage_components/sage_expandable_card.rb +++ b/docs/lib/sage_rails/app/sage_components/sage_expandable_card.rb @@ -1,10 +1,15 @@ class SageExpandableCard < SageComponent set_attribute_schema({ align_arrow_right: [:optional, NilClass, TrueClass], + align_trigger: [:optional, NilClass, String], body_bordered: [:optional, NilClass, TrueClass], - sage_type: [:optional, NilClass, TrueClass], expanded: [:optional, NilClass, TrueClass], + sage_type: [:optional, NilClass, TrueClass], trigger_label: [:optional, NilClass, String], }) + + def sections + %w(expandable_card_custom_header ) + end end diff --git a/docs/lib/sage_rails/app/views/sage_components/_sage_expandable_card.html.erb b/docs/lib/sage_rails/app/views/sage_components/_sage_expandable_card.html.erb index 9c85bf7df8..8c581c99f8 100644 --- a/docs/lib/sage_rails/app/views/sage_components/_sage_expandable_card.html.erb +++ b/docs/lib/sage_rails/app/views/sage_components/_sage_expandable_card.html.erb @@ -1,21 +1,41 @@ -
<% if component.expanded.present? %>sage-expandable-card--expanded<% end %> <% if component.align_arrow_right %>sage-expandable-card--align-arrow-right<% end %>" <%= component.generated_html_attributes.html_safe %> > - <%= sage_component SageButton, { - style: "secondary", - subtle: true, - value: component.trigger_label, - icon: { name: "caret-right", style: "left" }, - css_classes: "sage-expandable-card__trigger", - attributes: { - "data-js-accordion": "header", - "aria-expanded": (component.expanded.present? ? true : false) - }, - } %> + <% if component.align_trigger == "right" || component.align_trigger == "left" %> +
+ <%= sage_component SageButton, { + style: "secondary", + subtle: true, + value: component.trigger_label, + icon: { name: "caret-right", style: "only" }, + css_classes: "sage-expandable-card__trigger", + attributes: { + "data-js-accordion": "header", + "aria-expanded": (component.expanded.present? ? true : false) + }, + } %> +
+ <% if content_for? :sage_expandable_card_custom_header %> + <%= content_for :sage_expandable_card_custom_header %> + <% end %> +
+
+ <% else %> + <%= sage_component SageButton, { + style: "secondary", + subtle: true, + value: component.trigger_label, + icon: { name: "caret-right", style: "left" }, + css_classes: "sage-expandable-card__trigger", + attributes: { + "data-js-accordion": "header", + "aria-expanded": (component.expanded.present? ? true : false) + }, + } %> + <% end %>
diff --git a/docs/lib/sage_rails/app/views/sage_components/_sage_form_select.html.erb b/docs/lib/sage_rails/app/views/sage_components/_sage_form_select.html.erb index 9acb0404c3..b8c5652a7e 100644 --- a/docs/lib/sage_rails/app/views/sage_components/_sage_form_select.html.erb +++ b/docs/lib/sage_rails/app/views/sage_components/_sage_form_select.html.erb @@ -17,6 +17,7 @@ name="<%= select_id %>" id="<%= select_id %>" <%= "disabled" if component.disabled %> + <%= component.generated_component_attributes.html_safe %> > <% if component.select_options.present? %> <% component.select_options.each do |option| %> diff --git a/packages/sage-assets/lib/stylesheets/components/_expandable_card.scss b/packages/sage-assets/lib/stylesheets/components/_expandable_card.scss index 5d245b26fa..468ddc897b 100644 --- a/packages/sage-assets/lib/stylesheets/components/_expandable_card.scss +++ b/packages/sage-assets/lib/stylesheets/components/_expandable_card.scss @@ -19,6 +19,12 @@ $-expandable-card-padding-xs: sage-spacing(xs); &:not(:last-child) { margin-bottom: sage-spacing(); } + + .sage-expandable-card__header { + padding: sage-spacing(); + padding-bottom: 0; + margin-bottom: -#{sage-spacing(xs)}; + } } } @@ -86,6 +92,8 @@ $-expandable-card-padding-xs: sage-spacing(xs); height: 100%; border-radius: rem(14px); } + + } } @@ -95,3 +103,43 @@ $-expandable-card-padding-xs: sage-spacing(xs); } } // NOTE: Styles for align_arrow_right prop added in `_button` + +// styles related to custom header +.sage-expandable-card__header { + display: grid; + grid-auto-columns: auto; + grid-auto-flow: column; + align-items: center; + gap: $-expandable-card-padding; + .sage-expandable-card__trigger::before { + margin: 0; + } +} +.sage-expandable-card__trigger-left { + grid-template-columns: auto 1fr; +} +.sage-expandable-card__trigger-right { + grid-template-columns: 1fr auto; + .sage-expandable-card__trigger { + order: 1; + } +} + + +// styles related to custom header when used in accordian +.sage-accordion { + .sage-expandable-card__header { + .sage-btn--subtle::after { + width: rem(32px); + height: rem(32px); + padding: inherit; + border-radius: sage-border(radius-round); + } + .sage-expandable-card__trigger { + align-items: center; + justify-content: center; + padding: 0; + border-radius: sage-border(radius-round); + } + } +} diff --git a/packages/sage-react/lib/ExpandableCard/ExpandableCard.jsx b/packages/sage-react/lib/ExpandableCard/ExpandableCard.jsx index f59657143a..2109efdaae 100644 --- a/packages/sage-react/lib/ExpandableCard/ExpandableCard.jsx +++ b/packages/sage-react/lib/ExpandableCard/ExpandableCard.jsx @@ -7,10 +7,12 @@ import { SageClassnames, SageTokens } from '../configs'; export const ExpandableCard = ({ alignArrowRight, + alignTrigger, bodyBordered, expanded, children, className, + headerContent, name, onClick, sageType, @@ -36,7 +38,6 @@ export const ExpandableCard = ({ 'sage-expandable-card', { 'sage-expandable-card--align-arrow-right': alignArrowRight, - 'sage-expandable-card': !isExpanded, 'sage-expandable-card--expanded': isExpanded } ); @@ -47,8 +48,9 @@ export const ExpandableCard = ({ [`${SageClassnames.TYPE_BLOCK}`]: sageType, }); - return ( -
+ const determineAlignment = () => { + const className = 'sage-expandable-card__header'; + const ButtonWrapper = ({ ...rest }) => ( + ); + + if (alignTrigger === 'middle') { + return ( + <> + + + ); + } + return ( +
+ +
{headerContent}
+
+ ); + }; + + return ( +
+ {determineAlignment()} +
{children}
@@ -74,6 +98,8 @@ ExpandableCard.defaultProps = { expanded: false, children: null, className: null, + headerContent: null, + alignTrigger: 'middle', name: null, onClick: null, sageType: false, @@ -83,9 +109,11 @@ ExpandableCard.defaultProps = { ExpandableCard.propTypes = { alignArrowRight: PropTypes.bool, bodyBordered: PropTypes.bool, + headerContent: PropTypes.node, expanded: PropTypes.bool, className: PropTypes.string, children: PropTypes.node, + alignTrigger: PropTypes.oneOf(['left', 'middle', 'right']), name: PropTypes.string, onClick: PropTypes.func, sageType: PropTypes.bool, diff --git a/packages/sage-react/lib/ExpandableCard/ExpandableCard.story.jsx b/packages/sage-react/lib/ExpandableCard/ExpandableCard.story.jsx index f01ad34f68..b74c5cf423 100644 --- a/packages/sage-react/lib/ExpandableCard/ExpandableCard.story.jsx +++ b/packages/sage-react/lib/ExpandableCard/ExpandableCard.story.jsx @@ -1,6 +1,10 @@ import React from 'react'; import { Checkbox } from '../Toggle'; import { ExpandableCard } from './ExpandableCard'; +import { Badge } from '../Badge'; +import { OptionsDropdown } from '../Dropdown'; +import { SageClassnames } from '../configs'; +import { Grid } from '../Grid'; export default { title: 'Sage/ExpandableCard', @@ -9,7 +13,8 @@ export default { parameters: { docs: { description: { - component: 'Card that can be expanded and collapsed in order to display additional content.' + component: + 'Card that can be expanded and collapsed in order to display additional content.', }, }, }, @@ -44,9 +49,60 @@ export default { /> ), - triggerLabel: 'Expand' - } + triggerLabel: 'Expand', + }, }; const Template = (args) => ; export const Default = Template.bind({}); + +export const CustomHeader = () => ( + <> + + + + + ¥1,107,243.69 JPY ($8,000.50 USD) + + + + + {}} + isPinned={false} + onEscapeHook={() => {}} + triggerButtonSubtle={true} + options={[ + { + id: 1, + label: 'Send reciept', + }, + { + id: 2, + label: 'View receipt', + }, + { + id: 3, + label: 'Refund payment', + }, + ]} + /> + + + + + )} + > +
This is the content
+
+ +); diff --git a/packages/sage-react/lib/ExpandableCard/ExpandableCardNotes.story.mdx b/packages/sage-react/lib/ExpandableCard/ExpandableCardNotes.story.mdx new file mode 100644 index 0000000000..9ed0c383af --- /dev/null +++ b/packages/sage-react/lib/ExpandableCard/ExpandableCardNotes.story.mdx @@ -0,0 +1,17 @@ +import { Meta } from "@storybook/addon-docs"; + + + +### Custom Header: + +The `ExpandableCard` component has two props that can be used together to create a custom header solution: `alignTrigger` and `headerContent`. + +** `alignTrigger` prop: ** + +The `alignTrigger` prop allows you to control the alignment of the trigger button inside the header. When set to "left", the trigger button will be aligned to the left side of the header. Conversely, when set to "right", the trigger button will be aligned to the right side of the header. + +** `headerContent` prop: ** + +The `headerContent` prop lets you provide custom content to be displayed alongside the trigger button in the header. This content can be any valid React node, such as HTML elements, text, or other React components. + +By using these two props together, you can create a custom header layout for the `ExpandableCard` component. diff --git a/packages/sage-system/lib/accordion-panel.js b/packages/sage-system/lib/accordion-panel.js index 9527e5513b..c57e6ff186 100644 --- a/packages/sage-system/lib/accordion-panel.js +++ b/packages/sage-system/lib/accordion-panel.js @@ -48,7 +48,7 @@ Sage.accordion = (function () { // Toggle target const toggle = el.getAttribute('aria-expanded') === 'true'; el.setAttribute('aria-expanded', !toggle); - el.parentNode.classList.toggle('sage-expandable-card--expanded'); + el.closest('.sage-expandable-card').classList.toggle('sage-expandable-card--expanded') } // In a single panel accordion, this closes all panels that are not the current target @@ -66,7 +66,7 @@ Sage.accordion = (function () { function init(el) { const header = el; - const body = el.parentNode.querySelector(`[${JS_ACCORDION_ROOT}="${JS_ACCORDION_BODY}"]`); + const body = el.closest('.sage-expandable-card').querySelector(`[${JS_ACCORDION_ROOT}="${JS_ACCORDION_BODY}"]`); // Ensure there's a corresponding body if (!body) {